{"id":262370,"date":"2025-05-29T10:06:18","date_gmt":"2025-05-29T10:06:18","guid":{"rendered":"https:\/\/peraltafinancing.com\/business\/marketing\/how-to-automate-seo-keyword-clustering-by-search-intent-with-python\/"},"modified":"2025-05-29T10:06:18","modified_gmt":"2025-05-29T10:06:18","slug":"how-to-automate-seo-keyword-clustering-by-search-intent-with-python","status":"publish","type":"post","link":"https:\/\/fivemor.com\/?p=262370","title":{"rendered":"How To Automate SEO Keyword Clustering By Search Intent With Python"},"content":{"rendered":"<p> <br \/>\n<\/p>\n<div id=\"narrow-cont\">\n<p>There\u2019s a lot to know about search intent, from using deep learning to infer search intent by classifying text and breaking down SERP titles using Natural Language Processing (NLP) techniques, to clustering based on <a href=\"https:\/\/www.searchenginejournal.com\/sentence-level-semantic-internal-links-strategy\/506771\/\">semantic relevance<\/a>, with the benefits explained.<\/p>\n<p>Not only do we know the benefits of deciphering search intent, but we also have a number of techniques at our disposal for scale and automation.<\/p>\n<p>So, why do we need another article on automating search intent?<\/p>\n<p>Search intent is ever more important now that <a href=\"https:\/\/www.searchenginejournal.com\/how-llms-interpret-content-structure-information-for-ai-search\/544308\/\">AI search<\/a> has arrived.<\/p>\n<p>While more was generally in the 10 blue links search era, the opposite is true with AI search technology, as these platforms generally seek to minimize the computing costs (per FLOP) in order to deliver the service.<\/p>\n<h2>SERPs Still Contain The Best Insights For Search Intent<\/h2>\n<p>The techniques so far involve doing your own AI, that is, getting all of the copy from titles of the ranking content for a given keyword and then feeding it into a neural network model (which you have to then build and test) or using NLP to cluster keywords.<\/p>\n<p>What if you don\u2019t have time or the knowledge to build your own AI or invoke the Open AI API?<\/p>\n<p>While cosine similarity has been touted as the answer to helping SEO professionals navigate the demarcation of topics for <a href=\"https:\/\/www.searchenginejournal.com\/complete-guide-site-taxonomy-seo\/461241\/\">taxonomy<\/a> and site structures, I still maintain that search clustering by SERP results is a far superior method.<\/p>\n<p>That\u2019s because AI is very keen to ground its results on SERPs and for good reason \u2013 it\u2019s modelled on user behaviors.<\/p>\n<p>There is another way that uses Google\u2019s very own AI to do the work for you, without having to scrape all the SERPs content and build an AI model.<\/p>\n<p>Let\u2019s assume that Google ranks site URLs by the likelihood of the content satisfying the user query in descending order. It follows that if the intent for two keywords is the same, then the SERPs are likely to be similar.<\/p>\n<p>For years, many SEO professionals compared SERP results for <a href=\"https:\/\/www.searchenginejournal.com\/keyword-mapping-beginners-guide\/466483\/\">keywords<\/a> to infer shared (or shared) search intent to stay on top of core updates, so this is nothing new.<\/p>\n<p>The value-add here is the automation and scaling of this comparison, offering both speed and greater precision.<\/p>\n<h2>How To Cluster Keywords By Search Intent At Scale Using Python (With Code)<\/h2>\n<p>Assuming you have your SERPs results in a CSV download, let\u2019s import it into your Python notebook.<\/p>\n<h3>1. Import The List Into Your Python Notebook<\/h3>\n<div class=\"hcb_wrap\">\n<pre class=\"prism line-numbers lang-python\" data-lang=\"Python\"><code>import pandas as pd&#13;\nimport numpy as np&#13;\n&#13;\nserps_input = pd.read_csv('data\/sej_serps_input.csv')&#13;\ndel serps_input['Unnamed: 0']&#13;\nserps_input&#13;\n<\/code><\/pre>\n<\/div>\n<p>Below is the SERPs file now imported into a Pandas dataframe.<\/p>\n<div id=\"attachment_545598\" style=\"width: 1340px\" class=\"wp-caption aligncenter\"><img decoding=\"async\" src=\"https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2025\/04\/image1-612.png\" alt=\"\" width=\"1330\" height=\"800\" class=\"size-full wp-image-545598\" srcset=\"https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2025\/04\/image1-612.png 1330w, https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2025\/04\/image1-612-480x289.png 480w, https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2025\/04\/image1-612-680x409.png 680w, https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2025\/04\/image1-612-384x231.png 384w, https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2025\/04\/image1-612-768x462.png 768w, https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2025\/04\/image1-612-1024x616.png 1024w\" sizes=\"auto, (max-width: 1330px) 100vw, 1330px\" loading=\"lazy\"\/><span class=\"wp-caption-text\">Image from author, April 2025<\/span><\/div>\n<h3>2. Filter Data For Page 1<\/h3>\n<p>We want to compare the Page 1 results of each SERP between keywords.<\/p>\n<p>We\u2019ll split the dataframe into mini keyword dataframes to run the filtering function before recombining into a single dataframe, because we want to filter at the keyword level:<\/p>\n<div class=\"hcb_wrap\">\n<pre class=\"prism line-numbers lang-python\" data-lang=\"Python\"><code># Split &#13;\nserps_grpby_keyword = serps_input.groupby(\"keyword\")&#13;\nk_urls = 15&#13;\n&#13;\n# Apply Combine&#13;\ndef filter_k_urls(group_df):&#13;\n    filtered_df = group_df.loc[group_df['url'].notnull()]&#13;\n    filtered_df = filtered_df.loc[filtered_df['rank'] <\/code><\/pre>\n<\/div>\n<div id=\"attachment_413762\" style=\"width: 1256px\" class=\"wp-caption aligncenter\"><img decoding=\"async\" class=\"wp-image-413762 size-full\" src=\"https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2021\/07\/1_serps_import-60f4192d0f125-sej.png\" alt=\"SERPs file imported into a Pandas dataframe.\" width=\"1246\" height=\"784\" srcset=\"https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2021\/07\/1_serps_import-60f4192d0f125-sej.png 1246w, https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2021\/07\/1_serps_import-60f4192d0f125-sej-480x302.png 480w, https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2021\/07\/1_serps_import-60f4192d0f125-sej-680x428.png 680w, https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2021\/07\/1_serps_import-60f4192d0f125-sej-768x483.png 768w, https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2021\/07\/1_serps_import-60f4192d0f125-sej-1024x644.png 1024w\" sizes=\"auto, (max-width: 1246px) 100vw, 1246px\" loading=\"lazy\"\/><span class=\"wp-caption-text\">Image from author, April 2025<\/span><\/div>\n<pre\/>\n<h3>3. Convert Ranking URLs To A String<\/h3>\n<p>Because there are more SERP result URLs than keywords, we need to compress those URLs into a single line to represent the keyword\u2019s SERP.<\/p>\n<p>Here\u2019s how:<\/p>\n<div class=\"hcb_wrap\">\n<pre class=\"prism line-numbers lang-python\" data-lang=\"Python\"><code>&#13;\n# convert results to strings using Split Apply Combine &#13;\nfiltserps_grpby_keyword = filtered_serps_df.groupby(\"keyword\")&#13;\n&#13;\ndef string_serps(df): &#13;\n   df['serp_string'] = ''.join(df['url'])&#13;\n   return df # Combine strung_serps = filtserps_grpby_keyword.apply(string_serps) &#13;\n&#13;\n# Concatenate with initial data frame and clean &#13;\nstrung_serps = pd.concat([strung_serps],axis=0) &#13;\nstrung_serps = strung_serps[['keyword', 'serp_string']]#.head(30) &#13;\nstrung_serps = strung_serps.drop_duplicates() &#13;\nstrung_serps&#13;\n<\/code><\/pre>\n<\/div>\n<p>Below shows the SERP compressed into a single line for each keyword.<\/p>\n<div id=\"attachment_413763\" style=\"width: 1128px\" class=\"wp-caption aligncenter\"><img decoding=\"async\" class=\"wp-image-413763 size-full\" src=\"https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2021\/07\/2_serp_strung-60f41930648be-sej.png\" alt=\"SERP compressed into single line for each keyword.\" width=\"1118\" height=\"942\" srcset=\"https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2021\/07\/2_serp_strung-60f41930648be-sej.png 1118w, https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2021\/07\/2_serp_strung-60f41930648be-sej-480x404.png 480w, https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2021\/07\/2_serp_strung-60f41930648be-sej-680x573.png 680w, https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2021\/07\/2_serp_strung-60f41930648be-sej-768x647.png 768w, https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2021\/07\/2_serp_strung-60f41930648be-sej-1024x863.png 1024w\" sizes=\"auto, (max-width: 1118px) 100vw, 1118px\" loading=\"lazy\"\/><span class=\"wp-caption-text\">Image from author, April 2025<\/span><\/div>\n<h3>4. Compare SERP Distance<\/h3>\n<p>To perform the comparison, we now need every combination of keyword SERP paired with other pairs:<\/p>\n<div class=\"hcb_wrap\">\n<pre class=\"prism line-numbers lang-python\" data-lang=\"Python\"><code>&#13;\n# align serps&#13;\ndef serps_align(k, df):&#13;\n    prime_df = df.loc[df.keyword == k]&#13;\n    prime_df = prime_df.rename(columns = {\"serp_string\" : \"serp_string_a\", 'keyword': 'keyword_a'})&#13;\n    comp_df = df.loc[df.keyword != k].reset_index(drop=True)&#13;\n    prime_df = prime_df.loc[prime_df.index.repeat(len(comp_df.index))].reset_index(drop=True)&#13;\n    prime_df = pd.concat([prime_df, comp_df], axis=1)&#13;\n    prime_df = prime_df.rename(columns = {\"serp_string\" : \"serp_string_b\", 'keyword': 'keyword_b', \"serp_string_a\" : \"serp_string\", 'keyword_a': 'keyword'})&#13;\n    return prime_df&#13;\n&#13;\ncolumns = ['keyword', 'serp_string', 'keyword_b', 'serp_string_b']&#13;\nmatched_serps = pd.DataFrame(columns=columns)&#13;\nmatched_serps = matched_serps.fillna(0)&#13;\nqueries = strung_serps.keyword.to_list()&#13;\n&#13;\nfor q in queries:&#13;\n    temp_df = serps_align(q, strung_serps)&#13;\n    matched_serps = matched_serps.append(temp_df)&#13;\n&#13;\nmatched_serps&#13;\n<\/code><\/pre>\n<\/div>\n<pre\/>\n<p><img decoding=\"async\" class=\"aligncenter wp-image-413764 size-full\" src=\"https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2021\/07\/3_serps_aligned-60f41934833be-sej.png\" alt=\"Compare SERP similarity.\" width=\"1940\" height=\"794\" srcset=\"https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2021\/07\/3_serps_aligned-60f41934833be-sej.png 1940w, https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2021\/07\/3_serps_aligned-60f41934833be-sej-480x196.png 480w, https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2021\/07\/3_serps_aligned-60f41934833be-sej-680x278.png 680w, https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2021\/07\/3_serps_aligned-60f41934833be-sej-768x314.png 768w, https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2021\/07\/3_serps_aligned-60f41934833be-sej-1024x419.png 1024w, https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2021\/07\/3_serps_aligned-60f41934833be-sej-1600x655.png 1600w\" sizes=\"auto, (max-width: 1940px) 100vw, 1940px\" loading=\"lazy\"\/><\/p>\n<p>The above shows all of the keyword SERP pair combinations, making it ready for SERP string comparison.<\/p>\n<p>There is no open-source library that compares list objects by order, so the function has been written for you below.<\/p>\n<p>The function \u201cserp_compare\u201d compares the overlap of sites and the order of those sites between SERPs.<\/p>\n<div class=\"hcb_wrap\">\n<pre class=\"prism line-numbers lang-python\" data-lang=\"Python\"><code>&#13;\nimport py_stringmatching as sm&#13;\nws_tok = sm.WhitespaceTokenizer()&#13;\n&#13;\n# Only compare the top k_urls results &#13;\ndef serps_similarity(serps_str1, serps_str2, k=15):&#13;\n    denom = k+1&#13;\n    norm = sum([2*(1\/i - 1.0\/(denom)) for i in range(1, denom)])&#13;\n    #use to tokenize the URLs&#13;\n    ws_tok = sm.WhitespaceTokenizer()&#13;\n    #keep only first k URLs&#13;\n    serps_1 = ws_tok.tokenize(serps_str1)[:k]&#13;\n    serps_2 = ws_tok.tokenize(serps_str2)[:k]&#13;\n    #get positions of matches &#13;\n    match = lambda a, b: [b.index(x)+1 if x in b else None for x in a]&#13;\n    #positions intersections of form [(pos_1, pos_2), ...]&#13;\n    pos_intersections = [(i+1,j) for i,j in enumerate(match(serps_1, serps_2)) if j is not None] &#13;\n    pos_in1_not_in2 = [i+1 for i,j in enumerate(match(serps_1, serps_2)) if j is None]&#13;\n    pos_in2_not_in1 = [i+1 for i,j in enumerate(match(serps_2, serps_1)) if j is None]&#13;\n    &#13;\n    a_sum = sum([abs(1\/i -1\/j) for i,j in pos_intersections])&#13;\n    b_sum = sum([abs(1\/i -1\/denom) for i in pos_in1_not_in2])&#13;\n    c_sum = sum([abs(1\/i -1\/denom) for i in pos_in2_not_in1])&#13;\n&#13;\n    intent_prime = a_sum + b_sum + c_sum&#13;\n    intent_dist = 1 - (intent_prime\/norm)&#13;\n    return intent_dist&#13;\n&#13;\n# Apply the function&#13;\nmatched_serps['si_simi'] = matched_serps.apply(lambda x: serps_similarity(x.serp_string, x.serp_string_b), axis=1)&#13;\n&#13;\n# This is what you get&#13;\nmatched_serps[['keyword', 'keyword_b', 'si_simi']]&#13;\n<\/code><\/pre>\n<\/div>\n<pre\/>\n<p><img decoding=\"async\" class=\"aligncenter wp-image-413766 size-full\" src=\"https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2021\/07\/5_serps_map-60f4193cd19d4-sej.png\" alt=\"Overlap of sites and the order of those sites between SERPs.\" width=\"782\" height=\"800\" srcset=\"https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2021\/07\/5_serps_map-60f4193cd19d4-sej.png 782w, https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2021\/07\/5_serps_map-60f4193cd19d4-sej-480x491.png 480w, https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2021\/07\/5_serps_map-60f4193cd19d4-sej-680x696.png 680w, https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2021\/07\/5_serps_map-60f4193cd19d4-sej-768x786.png 768w\" sizes=\"auto, (max-width: 782px) 100vw, 782px\" loading=\"lazy\"\/><\/p>\n<p>Now that the comparisons have been executed, we can start clustering keywords.<\/p>\n<p>We will be treating any keywords that have a weighted similarity of 40% or more.<\/p>\n<div class=\"hcb_wrap\">\n<pre class=\"prism line-numbers lang-python\" data-lang=\"Python\"><code>&#13;\n# group keywords by search intent&#13;\nsimi_lim = 0.4&#13;\n&#13;\n# join search volume&#13;\nkeysv_df = serps_input[['keyword', 'search_volume']].drop_duplicates()&#13;\nkeysv_df.head()&#13;\n&#13;\n# append topic vols&#13;\nkeywords_crossed_vols = serps_compared.merge(keysv_df, on = 'keyword', how = 'left')&#13;\nkeywords_crossed_vols = keywords_crossed_vols.rename(columns = {'keyword': 'topic', 'keyword_b': 'keyword',&#13;\n                                                                'search_volume': 'topic_volume'})&#13;\n&#13;\n# sim si_simi&#13;\nkeywords_crossed_vols.sort_values('topic_volume', ascending = False)&#13;\n&#13;\n# strip NAN&#13;\nkeywords_filtered_nonnan = keywords_crossed_vols.dropna()&#13;\nkeywords_filtered_nonnan&#13;\n<\/code><\/pre>\n<\/div>\n<p>We now have the potential topic name, keywords SERP similarity, and search volumes of each.<br \/><img decoding=\"async\" class=\"aligncenter wp-image-413767 size-full\" src=\"https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2021\/07\/6_topic_keywords-60f4193f69848-sej.png\" alt=\"Clustering keywords.\" width=\"982\" height=\"704\" srcset=\"https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2021\/07\/6_topic_keywords-60f4193f69848-sej.png 982w, https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2021\/07\/6_topic_keywords-60f4193f69848-sej-480x344.png 480w, https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2021\/07\/6_topic_keywords-60f4193f69848-sej-680x487.png 680w, https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2021\/07\/6_topic_keywords-60f4193f69848-sej-768x551.png 768w\" sizes=\"auto, (max-width: 982px) 100vw, 982px\" loading=\"lazy\"\/><\/p>\n<p>You\u2019ll note that keyword and keyword_b have been renamed to topic and keyword, respectively.<\/p>\n<p>Now we\u2019re going to iterate over the columns in the dataframe using the lambda technique.<\/p>\n<p>The lambda technique is an efficient way to iterate over rows in a Pandas dataframe because it converts rows to a list as opposed to the .iterrows() function.<\/p>\n<p>Here goes:<\/p>\n<div class=\"hcb_wrap\">\n<pre class=\"prism line-numbers lang-python\" data-lang=\"Python\"><code>&#13;\nqueries_in_df = list(set(matched_serps['keyword'].to_list()))&#13;\ntopic_groups = {}&#13;\n&#13;\ndef dict_key(dicto, keyo):&#13;\n    return keyo in dicto&#13;\n&#13;\ndef dict_values(dicto, vala):&#13;\n    return any(vala in val for val in dicto.values())&#13;\n&#13;\ndef what_key(dicto, vala):&#13;\n    for k, v in dicto.items():&#13;\n            if vala in v:&#13;\n                return k&#13;\n&#13;\ndef find_topics(si, keyw, topc):&#13;\n    if (si &gt;= simi_lim):&#13;\n&#13;\n        if (not dict_key(sim_topic_groups, keyw)) and (not dict_key(sim_topic_groups, topc)): &#13;\n&#13;\n            if (not dict_values(sim_topic_groups, keyw)) and (not dict_values(sim_topic_groups, topc)): &#13;\n                sim_topic_groups[keyw] = [keyw] &#13;\n                sim_topic_groups[keyw] = [topc] &#13;\n                if dict_key(non_sim_topic_groups, keyw):&#13;\n                    non_sim_topic_groups.pop(keyw)&#13;\n                if dict_key(non_sim_topic_groups, topc): &#13;\n                    non_sim_topic_groups.pop(topc)&#13;\n            if (dict_values(sim_topic_groups, keyw)) and (not dict_values(sim_topic_groups, topc)): &#13;\n                d_key = what_key(sim_topic_groups, keyw)&#13;\n                sim_topic_groups[d_key].append(topc)&#13;\n                if dict_key(non_sim_topic_groups, keyw):&#13;\n                    non_sim_topic_groups.pop(keyw)&#13;\n                if dict_key(non_sim_topic_groups, topc): &#13;\n                    non_sim_topic_groups.pop(topc)&#13;\n            if (not dict_values(sim_topic_groups, keyw)) and (dict_values(sim_topic_groups, topc)): &#13;\n                d_key = what_key(sim_topic_groups, topc)&#13;\n                sim_topic_groups[d_key].append(keyw)&#13;\n                if dict_key(non_sim_topic_groups, keyw):&#13;\n                    non_sim_topic_groups.pop(keyw)&#13;\n                if dict_key(non_sim_topic_groups, topc): &#13;\n                    non_sim_topic_groups.pop(topc) &#13;\n&#13;\n        elif (keyw in sim_topic_groups) and (not topc in sim_topic_groups): &#13;\n            sim_topic_groups[keyw].append(topc)&#13;\n            sim_topic_groups[keyw].append(keyw)&#13;\n            if keyw in non_sim_topic_groups:&#13;\n                non_sim_topic_groups.pop(keyw)&#13;\n            if topc in non_sim_topic_groups: &#13;\n                non_sim_topic_groups.pop(topc)&#13;\n        elif (not keyw in sim_topic_groups) and (topc in sim_topic_groups):&#13;\n            sim_topic_groups[topc].append(keyw)&#13;\n            sim_topic_groups[topc].append(topc)&#13;\n            if keyw in non_sim_topic_groups:&#13;\n                non_sim_topic_groups.pop(keyw)&#13;\n            if topc in non_sim_topic_groups: &#13;\n                non_sim_topic_groups.pop(topc)&#13;\n        elif (keyw in sim_topic_groups) and (topc in sim_topic_groups):&#13;\n            if len(sim_topic_groups[keyw]) &gt; len(sim_topic_groups[topc]):&#13;\n                sim_topic_groups[keyw].append(topc) &#13;\n                [sim_topic_groups[keyw].append(x) for x in sim_topic_groups.get(topc)] &#13;\n                sim_topic_groups.pop(topc)&#13;\n&#13;\n        elif len(sim_topic_groups[keyw]) <\/code><\/pre>\n<\/div>\n<p>Below shows a dictionary containing all the keywords clustered by search intent into numbered groups:<\/p>\n<div class=\"hcb_wrap\">\n<pre class=\"prism line-numbers lang-python\" data-lang=\"Python\"><code>{1: ['fixed rate isa',&#13;\n  'isa rates',&#13;\n  'isa interest rates',&#13;\n  'best isa rates',&#13;\n  'cash isa',&#13;\n  'cash isa rates'],&#13;\n 2: ['child savings account', 'kids savings account'],&#13;\n 3: ['savings account',&#13;\n  'savings account interest rate',&#13;\n  'savings rates',&#13;\n  'fixed rate savings',&#13;\n  'easy access savings',&#13;\n  'fixed rate bonds',&#13;\n  'online savings account',&#13;\n  'easy access savings account',&#13;\n  'savings accounts uk'],&#13;\n 4: ['isa account', 'isa', 'isa savings']}<\/code><\/pre>\n<\/div>\n<p>Let\u2019s stick that into a dataframe:<\/p>\n<div class=\"hcb_wrap\">\n<pre class=\"prism line-numbers lang-python\" data-lang=\"Python\"><code>&#13;\ntopic_groups_lst = []&#13;\n&#13;\nfor k, l in topic_groups_numbered.items():&#13;\n    for v in l:&#13;\n        topic_groups_lst.append([k, v])&#13;\n&#13;\ntopic_groups_dictdf = pd.DataFrame(topic_groups_lst, columns=['topic_group_no', 'keyword'])&#13;\n                                &#13;\ntopic_groups_dictdf&#13;\n<\/code><\/pre>\n<\/div>\n<div id=\"attachment_413768\" style=\"width: 682px\" class=\"wp-caption aligncenter\"><img decoding=\"async\" class=\"wp-image-413768 size-full\" src=\"https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2021\/07\/7_keywords_clustered-60f41941c9baa-sej.png\" alt=\"Topic group dataframe.\" width=\"672\" height=\"1234\" srcset=\"https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2021\/07\/7_keywords_clustered-60f41941c9baa-sej.png 672w, https:\/\/www.searchenginejournal.com\/wp-content\/uploads\/2021\/07\/7_keywords_clustered-60f41941c9baa-sej-480x881.png 480w\" sizes=\"auto, (max-width: 672px) 100vw, 672px\" loading=\"lazy\"\/><span class=\"wp-caption-text\">Image from author, April 2025<\/span><\/div>\n<p>The search intent groups above show a good approximation of the keywords inside them, something that an SEO expert would likely achieve.<\/p>\n<p>Although we only used a small set of keywords, the method can obviously be scaled to thousands (if not more).<\/p>\n<h2>Activating The Outputs To Make Your Search Better<\/h2>\n<p>Of course, the above could be taken further using neural networks, processing the ranking content for more accurate clusters and cluster group naming, as some of the commercial products out there already do.<\/p>\n<p>For now, with this output, you can:<\/p>\n<ul>\n<li>Incorporate this into your own SEO dashboard systems to make your trends and <a href=\"https:\/\/www.searchenginejournal.com\/seo-reports-which-metrics-matter-how-to-use-them-well\/341839\/\">SEO reporting<\/a> more meaningful.<\/li>\n<li>Build better <a href=\"https:\/\/www.searchenginejournal.com\/ppc-best-practices\/274019\/\">paid search campaigns<\/a> by structuring your Google Ads accounts by search intent for a higher Quality Score.<\/li>\n<li>Merge redundant facet ecommerce search URLs.<\/li>\n<li>Structure a shopping site\u2019s taxonomy according to search intent instead of a typical product catalog.<\/li>\n<\/ul>\n<p>I\u2019m sure there are more applications that I haven\u2019t mentioned \u2013 feel free to comment on any important ones that I\u2019ve not already mentioned.<\/p>\n<p>In any case, your SEO keyword research just got that little bit more scalable, accurate, and quicker!<\/p>\n<p>Download the <a href=\"https:\/\/github.com\/seojournal\/cluster\/blob\/main\/cluster.ipynb\" target=\"_blank\" rel=\"noopener\">full code here for your own use<\/a>.<\/p>\n<p><strong>More Resources:<\/strong><\/p>\n<hr\/>\n<p><em>Featured Image: Buch and Bee\/Shutterstock<\/em><\/p>\n<\/div>\n\n","protected":false},"excerpt":{"rendered":"<p>There\u2019s a lot to know about search intent, from using deep learning to infer search intent by classifying text and breaking down SERP titles using Natural Language Processing (NLP) techniques, to clustering based on semantic relevance, with the benefits explained. Not only do we know the benefits of deciphering search intent, but we also have [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":262371,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[98],"tags":[11289,94214,28395,26453,21302,5476,12733],"dealstore":[],"offerexpiration":[],"class_list":["post-262370","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-marketing","tag-automate","tag-clustering","tag-intent","tag-keyword","tag-python","tag-search","tag-seo"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v26.4 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>How To Automate SEO Keyword Clustering By Search Intent With Python - Som2ny Network<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/fivemor.com\/?p=262370\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"How To Automate SEO Keyword Clustering By Search Intent With Python - Som2ny Network\" \/>\n<meta property=\"og:description\" content=\"There\u2019s a lot to know about search intent, from using deep learning to infer search intent by classifying text and breaking down SERP titles using Natural Language Processing (NLP) techniques, to clustering based on semantic relevance, with the benefits explained. Not only do we know the benefits of deciphering search intent, but we also have [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/fivemor.com\/?p=262370\" \/>\n<meta property=\"og:site_name\" content=\"Som2ny Network\" \/>\n<meta property=\"article:published_time\" content=\"2025-05-29T10:06:18+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/fivemor.com\/wp-content\/uploads\/2025\/05\/data-217.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1600\" \/>\n\t<meta property=\"og:image:height\" content=\"840\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"admin\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"admin\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"10 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/fivemor.com\/?p=262370#article\",\"isPartOf\":{\"@id\":\"https:\/\/fivemor.com\/?p=262370\"},\"author\":{\"name\":\"admin\",\"@id\":\"https:\/\/fivemor.com\/#\/schema\/person\/b85e3c3dc0e1daea076524dc8810c371\"},\"headline\":\"How To Automate SEO Keyword Clustering By Search Intent With Python\",\"datePublished\":\"2025-05-29T10:06:18+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/fivemor.com\/?p=262370\"},\"wordCount\":843,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\/\/fivemor.com\/#organization\"},\"image\":{\"@id\":\"https:\/\/fivemor.com\/?p=262370#primaryimage\"},\"thumbnailUrl\":\"https:\/\/fivemor.com\/wp-content\/uploads\/2025\/05\/data-217.png\",\"keywords\":[\"Automate\",\"Clustering\",\"Intent\",\"Keyword\",\"Python\",\"search\",\"SEO\"],\"articleSection\":[\"Marketing\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/fivemor.com\/?p=262370#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/fivemor.com\/?p=262370\",\"url\":\"https:\/\/fivemor.com\/?p=262370\",\"name\":\"How To Automate SEO Keyword Clustering By Search Intent With Python - Som2ny Network\",\"isPartOf\":{\"@id\":\"https:\/\/fivemor.com\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/fivemor.com\/?p=262370#primaryimage\"},\"image\":{\"@id\":\"https:\/\/fivemor.com\/?p=262370#primaryimage\"},\"thumbnailUrl\":\"https:\/\/fivemor.com\/wp-content\/uploads\/2025\/05\/data-217.png\",\"datePublished\":\"2025-05-29T10:06:18+00:00\",\"breadcrumb\":{\"@id\":\"https:\/\/fivemor.com\/?p=262370#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/fivemor.com\/?p=262370\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/fivemor.com\/?p=262370#primaryimage\",\"url\":\"https:\/\/fivemor.com\/wp-content\/uploads\/2025\/05\/data-217.png\",\"contentUrl\":\"https:\/\/fivemor.com\/wp-content\/uploads\/2025\/05\/data-217.png\",\"width\":1600,\"height\":840},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/fivemor.com\/?p=262370#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/fivemor.com\/?bp_activities=1\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"How To Automate SEO Keyword Clustering By Search Intent With Python\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/fivemor.com\/#website\",\"url\":\"https:\/\/fivemor.com\/\",\"name\":\"Som2ny Network\",\"description\":\"Daily Deals\",\"publisher\":{\"@id\":\"https:\/\/fivemor.com\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/fivemor.com\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/fivemor.com\/#organization\",\"name\":\"Som2ny Network\",\"url\":\"https:\/\/fivemor.com\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/fivemor.com\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/fivemor.com\/wp-content\/uploads\/2026\/07\/4a0953c4-logo-300x86-1.png\",\"contentUrl\":\"https:\/\/fivemor.com\/wp-content\/uploads\/2026\/07\/4a0953c4-logo-300x86-1.png\",\"width\":300,\"height\":86,\"caption\":\"Som2ny Network\"},\"image\":{\"@id\":\"https:\/\/fivemor.com\/#\/schema\/logo\/image\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\/\/fivemor.com\/#\/schema\/person\/b85e3c3dc0e1daea076524dc8810c371\",\"name\":\"admin\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/fivemor.com\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/729ae85bf62b9917e93538db2f2688ca?s=96&r=g&default=https%3A%2F%2Ffivemor.com%2Fwp-content%2Fplugins%2Fbuddypress-first-letter-avatar%2Fimages%2Fdefault%2F96%2Flatin_a.png\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/729ae85bf62b9917e93538db2f2688ca?s=96&r=g&default=https%3A%2F%2Ffivemor.com%2Fwp-content%2Fplugins%2Fbuddypress-first-letter-avatar%2Fimages%2Fdefault%2F96%2Flatin_a.png\",\"caption\":\"admin\"},\"sameAs\":[\"https:\/\/fivemor.com\"],\"url\":\"https:\/\/fivemor.com\/?author=1\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"How To Automate SEO Keyword Clustering By Search Intent With Python - Som2ny Network","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/fivemor.com\/?p=262370","og_locale":"en_US","og_type":"article","og_title":"How To Automate SEO Keyword Clustering By Search Intent With Python - Som2ny Network","og_description":"There\u2019s a lot to know about search intent, from using deep learning to infer search intent by classifying text and breaking down SERP titles using Natural Language Processing (NLP) techniques, to clustering based on semantic relevance, with the benefits explained. Not only do we know the benefits of deciphering search intent, but we also have [&hellip;]","og_url":"https:\/\/fivemor.com\/?p=262370","og_site_name":"Som2ny Network","article_published_time":"2025-05-29T10:06:18+00:00","og_image":[{"width":1600,"height":840,"url":"https:\/\/fivemor.com\/wp-content\/uploads\/2025\/05\/data-217.png","type":"image\/png"}],"author":"admin","twitter_card":"summary_large_image","twitter_misc":{"Written by":"admin","Est. reading time":"10 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/fivemor.com\/?p=262370#article","isPartOf":{"@id":"https:\/\/fivemor.com\/?p=262370"},"author":{"name":"admin","@id":"https:\/\/fivemor.com\/#\/schema\/person\/b85e3c3dc0e1daea076524dc8810c371"},"headline":"How To Automate SEO Keyword Clustering By Search Intent With Python","datePublished":"2025-05-29T10:06:18+00:00","mainEntityOfPage":{"@id":"https:\/\/fivemor.com\/?p=262370"},"wordCount":843,"commentCount":0,"publisher":{"@id":"https:\/\/fivemor.com\/#organization"},"image":{"@id":"https:\/\/fivemor.com\/?p=262370#primaryimage"},"thumbnailUrl":"https:\/\/fivemor.com\/wp-content\/uploads\/2025\/05\/data-217.png","keywords":["Automate","Clustering","Intent","Keyword","Python","search","SEO"],"articleSection":["Marketing"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/fivemor.com\/?p=262370#respond"]}]},{"@type":"WebPage","@id":"https:\/\/fivemor.com\/?p=262370","url":"https:\/\/fivemor.com\/?p=262370","name":"How To Automate SEO Keyword Clustering By Search Intent With Python - Som2ny Network","isPartOf":{"@id":"https:\/\/fivemor.com\/#website"},"primaryImageOfPage":{"@id":"https:\/\/fivemor.com\/?p=262370#primaryimage"},"image":{"@id":"https:\/\/fivemor.com\/?p=262370#primaryimage"},"thumbnailUrl":"https:\/\/fivemor.com\/wp-content\/uploads\/2025\/05\/data-217.png","datePublished":"2025-05-29T10:06:18+00:00","breadcrumb":{"@id":"https:\/\/fivemor.com\/?p=262370#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/fivemor.com\/?p=262370"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/fivemor.com\/?p=262370#primaryimage","url":"https:\/\/fivemor.com\/wp-content\/uploads\/2025\/05\/data-217.png","contentUrl":"https:\/\/fivemor.com\/wp-content\/uploads\/2025\/05\/data-217.png","width":1600,"height":840},{"@type":"BreadcrumbList","@id":"https:\/\/fivemor.com\/?p=262370#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/fivemor.com\/?bp_activities=1"},{"@type":"ListItem","position":2,"name":"How To Automate SEO Keyword Clustering By Search Intent With Python"}]},{"@type":"WebSite","@id":"https:\/\/fivemor.com\/#website","url":"https:\/\/fivemor.com\/","name":"Som2ny Network","description":"Daily Deals","publisher":{"@id":"https:\/\/fivemor.com\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/fivemor.com\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/fivemor.com\/#organization","name":"Som2ny Network","url":"https:\/\/fivemor.com\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/fivemor.com\/#\/schema\/logo\/image\/","url":"https:\/\/fivemor.com\/wp-content\/uploads\/2026\/07\/4a0953c4-logo-300x86-1.png","contentUrl":"https:\/\/fivemor.com\/wp-content\/uploads\/2026\/07\/4a0953c4-logo-300x86-1.png","width":300,"height":86,"caption":"Som2ny Network"},"image":{"@id":"https:\/\/fivemor.com\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/fivemor.com\/#\/schema\/person\/b85e3c3dc0e1daea076524dc8810c371","name":"admin","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/fivemor.com\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/729ae85bf62b9917e93538db2f2688ca?s=96&r=g&default=https%3A%2F%2Ffivemor.com%2Fwp-content%2Fplugins%2Fbuddypress-first-letter-avatar%2Fimages%2Fdefault%2F96%2Flatin_a.png","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/729ae85bf62b9917e93538db2f2688ca?s=96&r=g&default=https%3A%2F%2Ffivemor.com%2Fwp-content%2Fplugins%2Fbuddypress-first-letter-avatar%2Fimages%2Fdefault%2F96%2Flatin_a.png","caption":"admin"},"sameAs":["https:\/\/fivemor.com"],"url":"https:\/\/fivemor.com\/?author=1"}]}},"_links":{"self":[{"href":"https:\/\/fivemor.com\/index.php?rest_route=\/wp\/v2\/posts\/262370","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/fivemor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/fivemor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/fivemor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/fivemor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=262370"}],"version-history":[{"count":0,"href":"https:\/\/fivemor.com\/index.php?rest_route=\/wp\/v2\/posts\/262370\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/fivemor.com\/index.php?rest_route=\/wp\/v2\/media\/262371"}],"wp:attachment":[{"href":"https:\/\/fivemor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=262370"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/fivemor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=262370"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/fivemor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=262370"},{"taxonomy":"dealstore","embeddable":true,"href":"https:\/\/fivemor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fdealstore&post=262370"},{"taxonomy":"offerexpiration","embeddable":true,"href":"https:\/\/fivemor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fofferexpiration&post=262370"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}