{"id":19538996,"url":"https://github.com/russellwzr/geo_word_clouds","last_synced_at":"2026-06-04T23:31:34.626Z","repository":{"id":154899497,"uuid":"417379547","full_name":"Russellwzr/Geo_Word_Clouds","owner":"Russellwzr","description":"《Geo Word Clouds》paper implementation （my first vis project）https://ieeexplore.ieee.org/document/7465262","archived":false,"fork":false,"pushed_at":"2021-10-15T05:32:57.000Z","size":1585,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-02-26T04:41:50.946Z","etag":null,"topics":["paper-implementations","wordcloud","wordle","wordle-python"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Russellwzr.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-10-15T05:32:51.000Z","updated_at":"2024-05-20T12:06:40.000Z","dependencies_parsed_at":null,"dependency_job_id":"8a39c5a7-0fbb-427b-a442-e9ad8e605f87","html_url":"https://github.com/Russellwzr/Geo_Word_Clouds","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/Russellwzr/Geo_Word_Clouds","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Russellwzr%2FGeo_Word_Clouds","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Russellwzr%2FGeo_Word_Clouds/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Russellwzr%2FGeo_Word_Clouds/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Russellwzr%2FGeo_Word_Clouds/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Russellwzr","download_url":"https://codeload.github.com/Russellwzr/Geo_Word_Clouds/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Russellwzr%2FGeo_Word_Clouds/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33924832,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-04T02:00:06.755Z","response_time":64,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["paper-implementations","wordcloud","wordle","wordle-python"],"created_at":"2024-11-11T02:37:42.532Z","updated_at":"2026-06-04T23:31:34.613Z","avatar_url":"https://github.com/Russellwzr.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"﻿# Geo Word Clouds 论文复现 （2020.8）\n\n## 准备阶段\n* 地图准备：\n用 echarts.js 绘制山东省地图，并修改图片大小为 800×500 并保存，作为词云背景\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20200805102445741.png)\n* 初步确定 Geo Map 范围\n实验初始阶段的想法是用山东的17个地级市（这样做结果更加直观）作为输入的标签数据，来测试词云对地理信息的反映程度，所以先用 Matlab App 中的 Image Viewer 大致测取各地级市的范围（单位为像素点），结果如下\n\n```bash\nCity\tmin_x\tmax_x\tmin_y\tmax_y\n威海\t\t617\t\t742\t\t111\t\t209\n烟台\t\t471\t\t675\t\t81\t\t226\n青岛\t\t468\t\t594\t\t162\t\t342\n潍坊\t\t354\t\t509\t\t141\t\t324\n日照\t\t391\t\t480\t\t290\t\t401\n东营\t\t360\t\t448\t\t42\t\t183\n淄博\t\t311\t\t380\t\t147\t\t301\n临沂\t\t285\t\t439\t\t275\t\t479\n滨州\t\t276\t\t364\t\t26\t\t210\n莱芜\t\t281\t\t330\t\t235\t\t290\n济南\t\t186\t\t310\t\t115\t\t284\n泰安\t\t170\t\t335\t\t240\t\t321\n济宁\t\t153\t\t302\t\t299\t\t474\n枣庄\t\t235\t\t315\t\t375\t\t473\n德州\t\t144\t\t301\t\t79\t\t246\n聊城\t\t108\t\t209\t\t177\t\t319\n菏泽\t\t66\t\t187\t\t313\t\t460\n```\n* 最后根据各地市的面积比例以及限定的坐标范围来生成随机数据\n* 确定实验流程\n\n## 实验阶段\n**这一部分只简要介绍实验流程，实现的具体细节见代码实现部分** \n\n初始随机数据集在地图上的分布情况如下\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20200805103513216.png)\n首先使用 Python 中的 wordcloud 库对生成的随机数据进行可视化，结果如下：\n可以看出单词的分布比较随机杂乱，没有包含任何地理信息，且单词的大小几乎相同，难以读出频率信息\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20200805103735772.png)\n在不聚类的情况下，只加入地理信息因素，绘制的词云图如下：\n可以看出，虽然现在的词云图已反映出了地理信息，但在地图中为每个点放置一个单词，很难找到出现频率高的单词，同时相邻近的单词颜色比较相近，难以区分，视觉效果不佳\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20200805112808239.png)\n\n加入原论文中的颜色分配算法，结果如下：\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20200805112655321.png)\n\n之后对初始数据集进行 K-means 聚类，继续绘制词云图，结果如下：\n其中各单词的位置与实际地理位置相吻合，同时，单词的大小也在一定程度上反映了该地市的面积\n（由于这里只着重测试地理位置，聚类较少，所以地图形状空缺较大，用户可以自定义填充单词进行形状填充）\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20200805123817665.png)\n叠加图层观察可视化结果，可以看出，各聚类中心大部分被单词所覆盖，虽有个别点出现偏差，但也在可接受的范围之内。\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20200805124244944.png)\n接下来再对另一个的数据集进行测试：\n数据大小为 9935 ，执行 K-means 聚类后生成 126 个聚类\n运行结果如下：\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20200805150523816.png)\n再对几个出现频率较高的单词进行叠加图层比较，结果如下：\n\n**Edith**\n\n单词覆盖基本准确\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20200805150016106.png)\n**Alice**\n\n左边的聚类覆盖基本吻合，右边的聚类由于单词 Nick 在聚类中心，同时如果在聚类中心放置的话，由于 Alice 的字体较大，无法放置，所以偏移到了左边\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20200805150456625.png)\n**Bettina**\n\n左边的聚类基本吻合，右边的聚类由于与 Gasol 冲突，进行了左移调整\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20200805152032427.png)\n可视化衡量指标的结果将在下一部分进行描述\n\n\n## 代码实现部分\n\n### 生成初始数据集\n为了方便实验，这里的坐标单位均为像素，实际应用的话，只需将经纬度坐标按一定比例转换即可。\n输入山东省各地市的地理位置信息以及自己所指定的标签名称，来生成包含地理位置坐标的标签数据集\n\n使用 C++ 中的 random_device 随机数引擎生成服从正态分布的随机数据：\n**RandomData.cpp**\n```cpp\n#include \u003cbits/stdc++.h\u003e\nusing namespace std;\n\n//Geo Info\nstruct Region{\n\tint min_x,max_x,min_y,max_y,area;\n\tvoid Output(){\n\t\tcout\u003c\u003cmin_x\u003c\u003c\" \"\u003c\u003cmax_x\u003c\u003c\" \"\u003c\u003cmin_y\u003c\u003c\" \"\u003c\u003cmax_y\u003c\u003c\" \"\u003c\u003carea\u003c\u003cendl;\n\t}\n}Geo[20];\n\n//Tags Info\nstruct Tags{\n\tstring name;\n\tint num,sz;\n\tint idx[10];\n\tvoid Output(){\n\t\tcout\u003c\u003cnum\u003c\u003c\" \"\u003c\u003cname\u003c\u003c\" \";\n\t\tfor(int i=1;i\u003c=sz;i++){\n\t\t\tcout\u003c\u003cidx[i]\u003c\u003c\" \";\n\t\t}\n\t\tcout\u003c\u003cendl;\n\t}\n}tags[100];\n\nint main()\n{\n\tstring city,TagName;\n\tint min_x,max_x,min_y,max_y,area;\n\tifstream in;\n\tin.open(\"SDCities.txt\");\n\t//Input Geo Info\n\tfor(int i=1;i\u003c=17;i++){\n\t\tin\u003e\u003ecity;\n\t\tin\u003e\u003eGeo[i].min_x\u003e\u003eGeo[i].max_x\u003e\u003eGeo[i].min_y\u003e\u003eGeo[i].max_y\u003e\u003eGeo[i].area;\n\t}\n\tin.close();\n\t/*\n\tfor(int i=1;i\u003c=17;i++){\n\t\tGeo[i].Output();\n\t}\n\t*/\n\tfreopen(\"TagsInfoVer2.txt\",\"r\",stdin);\n\tfreopen(\"CityTags3.txt\",\"w\",stdout);\n\t// Total Points\n\tint Count=0;\n\t//Input Tags Region \n\tvector\u003cint\u003e a;\n\tstring str;\n\tstringstream ss;\n\tbool flag=false;\n\tfor(int i=1;i\u003c=60;i++){\n\t\tstr=\"\";\n\t\tgetline(cin,str);\n\t\tss.clear();\n\t\tss\u003c\u003cstr;\n\t\tss\u003e\u003etags[i].num;\n\t\tss\u003e\u003etags[i].name;\n\t\twhile(ss){\n\t\t\tint t;\n\t\t\tss\u003e\u003et;\n\t\t\ta.push_back(t);\n\t\t}\n\t\ta.pop_back();\n\t\ttags[i].sz=a.size();\n\t\tfor(int j=0;j\u003ca.size();j++){\n\t\t\ttags[i].idx[j+1]=a[j];\n\t\t}\n\t\ta.clear(); \n\t\tCount+=tags[i].num*tags[i].sz;\n\t\t//tags[i].Output();\n\t}\n\t//Generate Data\n\tfor(int i=1;i\u003c=60;i++){\n\t\tfor(int j=1;j\u003c=tags[i].sz;j++){\n\t\t\t// Frequency\n\t\t\tint num=((double)tags[i].num/(double)Count)*10000;\n\t\t\t//cout\u003c\u003cnum\u003c\u003cendl; \n\t\t\trandom_device rd;\n\t\t\tmt19937_64 eng(rd());\n\t\t\t// Coordinate\n\t\t\tuniform_int_distribution\u003cunsigned long long\u003e distrx(Geo[tags[i].idx[j]].min_x, Geo[tags[i].idx[j]].max_x); \n\t\t\tuniform_int_distribution\u003cunsigned long long\u003e distry(Geo[tags[i].idx[j]].min_y, Geo[tags[i].idx[j]].max_y); \n\t\t\t// Tags x y frequency\n\t\t\tfor(int k=0;k\u003cnum;k++){\n\t\t\t\tcout\u003c\u003ctags[i].name\u003c\u003c\" \"\u003c\u003cdistrx(eng)\u003c\u003c\" \"\u003c\u003cdistry(eng)\u003c\u003c\" \"\u003c\u003ctags[i].idx[j]\u003c\u003cendl;\n\t\t\t}\n\t\t}\n\t}\n\treturn 0;\n}\n```\n\n### 数据预处理\n* \u003cfont size=4\u003e**K-means 聚类**\u003c/font\u003e\n\n原文中只简单的提及了使用 K-means 算法对标签进行聚类，但没有具体说明实现的细节，而 K 值的选取对最后的聚类结果有着较大的影响。由于我们要在程序中对每一个标签都要执行 K-means 算法，所以，利用肘部法则，根据变化趋势选取 K 值是不可行的，或者通过不同 K 值轮廓系数的对比来选取合适的 K 值，但这样的话要对同一标签进行多次聚类后，再根据结果进行比较，时间开销偏大，**这里选择的是根据地理位置进行聚类**。即根据标签点所落在的区域来确定 K 值，比如某一标签落在了（威海，济南）那么我们选取 K 值为 2 即可，但节省时间的同时，这样也存在一些问题，比如某一标签落在了（济南，泰安），由于这两个区域距离很近，其实聚为一类结果可能是最佳的，现在解决这个问题的想法是：如果是做成交互式的自动可视化的应用的话，可以在绘制词云前，首先展示我们的聚类结果，然后用户可以根据聚类结果对某一标签进行调整。\n\n**K_means_Region_Cluster.m**\n```python\nclc,clear\n[A,B] = xlsread('CityTags.xlsx');\n[Geo,GeoName] = xlsread('Region.xlsx');\nGeo = Geo(:,1:4);\nnum = length(A);\npre = B(1);\nx = [];\ny = [];\nidx = [];\nC = [];\nD = [];\nRegion = [];\nTags = [];\nRange = [];\n% 单词方向 ：水平 = 1，竖直 = 0\nDir = [];\n% 第 cnt 个聚类\ncnt = 1;\n% 为输入信息 打上聚类标签\ncluster_points = [];\nfor i = 1:num\n    cur = B(i);\n    if ~isequal(pre,cur)\n        X = [x' y'];\n        Region = unique(Region);\n        Rsz = size(Region);\n        Region = [];\n        K = Rsz(2);\n        [idx,D] = kmeans(X,K);\n        for cluster = 1:K\n            CurCluster = [X(idx==cluster,1) X(idx==cluster,2)];\n            CurCluster_Size = size(CurCluster);\n            tmp = zeros(CurCluster_Size(1),1);\n            tmp = tmp + cnt;\n            cluster_points = [cluster_points;X(idx==cluster,1) X(idx==cluster,2) tmp];\n            direction = abs(PCA_Rotation(CurCluster'));\n            if direction \u003e 45\n                Dir = [Dir;0];\n            else\n                Dir = [Dir;1];\n            end\n            sz = sum(idx==cluster);\n            tmpX = sort(X(idx==cluster,:));\n            min_x = tmpX(ceil(0.05*sz),1);\n            max_x = tmpX(ceil(0.95*sz),1);\n            min_y = tmpX(ceil(0.05*sz),2);\n            max_y = tmpX(ceil(0.95*sz),2);\n            Range = [Range;min_x,max_x,min_y,max_y];\n            C = [C;D(cluster,1) D(cluster,2) sz cnt];\n            cnt = cnt + 1;\n            x = [];\n            y = [];\n            Tags = [Tags;string(pre)];\n        end\n    end\n    x = [x A(i,1)];\n    y = [y A(i,2)];\n    Region = [Region A(i,3)];\n    pre=cur;\nend\nX = [x' y'];\nRegion = unique(Region);\nRsz = size(Region);\nK = Rsz(2);\n[idx,D] = kmeans(X,K);\nfor cluster = 1:K\n    CurCluster = [X(idx==cluster,1) X(idx==cluster,2)];\n    CurCluster_Size = size(CurCluster);\n    tmp = zeros(CurCluster_Size(1),1);\n    tmp = tmp + cnt;\n    cluster_points = [cluster_points;X(idx==cluster,1) X(idx==cluster,2) tmp];\n    direction = abs(PCA_Rotation(CurCluster'));\n    if direction \u003e 45\n        Dir = [Dir;0];\n    else\n        Dir = [Dir;1];\n    end\n    sz = sum(idx==cluster);\n    Range = [Range;min(X(idx==cluster,1)),max(X(idx==cluster,1)),min(X(idx==cluster,2)),max(X(idx==cluster,2))];\n    C = [C;D(cluster,1) D(cluster,2) sz cnt];\n    cnt = cnt + 1;\n    x = [];\n    y = [];\n    Tags = [Tags;string(cur)];\nend\nMap = [];\nGeoSize = size(Geo);\nInfoSize = size(C);\nfor i = 1:InfoSize(1)\n    for j = 1:GeoSize(1)\n        cx = C(i,1);\n        cy = C(i,2);\n        if cx \u003e= Geo(j,1) \u0026\u0026 cx \u003c= Geo(j,2) \u0026\u0026 cy \u003e= Geo(j,3) \u0026\u0026 cy \u003c= Geo(j,4)\n            Map = [Map;string(GeoName(j))];\n            break;\n        end\n    end\nend\nxlswrite('CityTagsCluster1.xlsx', [Tags,C,Dir Map,Range], 'A1')\nxlswrite('CityTagsWithIndex1.xlsx',cluster_points,'A1')\n```\n\n* \u003cfont size=4\u003e **PCA 确定聚类主方向**\u003c/font\u003e\n论文中 Placement Algorithm 的第三步需要确定单词的初始方向，方法如下所示![在这里插入图片描述](https://img-blog.csdnimg.cn/20200805154950260.png)\n这里采用PCA算法来确定聚类的主方向：\n[参考](https://blog.csdn.net/miscclp/article/details/7479969)\n对协方差矩阵进行特征分解，按照特征值从大到小的顺序，有特征矩阵 V，其每一行对应一个特征向量，解方程 V * T = (1 0)'  向量 T 经过 V 的投影之后，在主方向上为 1，在垂直主方向上为 0。故 T 即指示了主方向。\n\n**PCA_Rotation.m**\n\n```python\nfunction [Dir,res] = PCA(Data)\n% 以[-pi/2,pi/2]之间的角度pi表示方向\n% 1.去除均值\n[buf K] = size(Data);\nmiu = mean(Data')';\nfor k=1:K\n    Data(:,k) = Data(:,k)-miu; \nend;\nsigma = zeros(2,2);\n% 2.计算协方差\nfor k=1:K\n    x = Data(:,k);\n    sigma = sigma+x*x';\nend;\nsigma = sigma/K;  \n% 3.特征分解\n[V,D] = eig(sigma);\nif (D(1,1)\u003cD(2,2)) % 把较大的特征值对应的向量挪到第一行\n     buf = V(1,:);\n     V(1,:) = V(2,:);\n     V(2,:) = buf;\nend;\n% 4.求解主方向向量\nres = inv(V)*[1;0];\nDir = atan(res(2)/res(1))/pi*180;\n```\n\n### 核心放置算法\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20200805160718959.png?)\n\n* \u003cfont size = 4\u003e **Step 1 : Load Data** \u003c/font\u003e\n\n```python\n# Load Geo Map\ndataf = pd.read_excel('GeoRegion.xlsx')\nwordmap = dataf.values\nsz = wordmap.shape\ngeomap = dict()\nfor i in range(0,sz[0]):\n    geomap[wordmap[i][0]] =[wordmap[i][1],wordmap[i][2],wordmap[i][3],wordmap[i][4]]\n\n# Load Data Set\n# Load Cluster Info\ndf = pd.read_excel('CityTagsClusterVer4.xlsx')\nwords = df.values\n# Load Tags labeled by cluster index\ndf2 = pd.read_excel('CityTagsWithIndex2.xlsx')\npoints = df2.values\ncluster_size = words.shape[0] + 10\ncluster_points = [[] for i in range(cluster_size)]\n\nfor i in range(0, points.shape[0]):\n    cluster_points[points[i][2]].append((points[i][0],points[i][1]))\n```\n* \u003cfont size = 4\u003e **Step 2 : Cluster** \u003c/font\u003e\n由 K_means_Region_Cluster.m 实现\n\n**WordCluster 类声明**\n```python\nclass WordCluster(object):\n    def __init__(self,word,x,y,num,direction,region,min_x,max_x,min_y,max_y,idx):\n        self.word = word\n        self.x = x\n        self.y = y\n        self.num = num\n        self.direction = direction\n        self.region = region\n        self.min_x = min_x\n        self.max_x = max_x\n        self.min_y = min_y\n        self.max_y = max_y\n        self.idx = idx\n\n    def __lt__(self, other):\n        return self\n\n    def __lt__(self, other):\n        return self.num \u003e other.num\n\n    def __str__(self):\n        return '(' + self.word + ', ' + str(self.x) + ', ' + str(self.y) + ', ' + str(self.num) + ', '+str(self.direction)+')'\n\n```\n\n* \u003cfont size = 4\u003e**Step 3 : Assign Attributes For Each Cluster** \u003c/font\u003e\n  * **(1) Choose the Font Size** \n    ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200805160130214.png)\n     求解 $A_m$\n\n```python\n# compute the area of M in pixel : Am\nimg = cv2.imread(\"SD.png\")\ngray = cv2.cvtColor(img, cv2.COLOR_BGRA2GRAY)\nh, w = gray.shape[:2]\nm = np.reshape(gray, [1, w * h])\nmean = m.sum() / (w * h)\nret, binary = cv2.threshold(gray, mean, 255, cv2.THRESH_BINARY)\nAm = len(binary[binary == 0])\n```\n\n\u0026nbsp;\u0026nbsp;\u0026nbsp;\u0026nbsp;\u0026nbsp;\u0026nbsp;\u0026nbsp;\u0026nbsp;\u0026nbsp;\u0026nbsp;\u0026nbsp;\u0026nbsp;求解 $Font Size$：\n\n```python\nfont_size = int((cmath.sqrt((freq * Am)/n).real))\n```\n\u0026nbsp;\u0026nbsp;\u0026nbsp;\u0026nbsp;\u0026nbsp;\u0026nbsp;\u0026nbsp;\u0026nbsp;\u0026nbsp;\u0026nbsp;\u0026nbsp;\u0026nbsp;**(2) Choose the Rotation**   由 PCA_Rotation.m 实现\n\n   * **\u003cfont size=4\u003eStep 4 : Greedy Placement\u003c/font\u003e**\n      ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200805160752201.png)\n      论文中提到，如果某个单词不能被放在它准确的位置，那么就缩小它，重新加入队列，关于放置的方法没有提及（只是说了尽可能覆盖聚类中更多的点)\n      下面就依照我自己对问题的理解来实现放置算法：\n      关于放置的具体位置我尝试了质心周围矩形范围内放置、按区域放置、按聚类结果放置，\n* 第一种方法，由于重点一直关注在质心，所以为了实现放置可能会出现很多冲突的单词由于被缩放多次直至小于 minimal size 被抛弃，导致很多单词没有被绘制，实验结果也证实了这种猜测，由于存在多次迭代，运行时间较长，且被绘制在词云上的单词数减少了\n* 第二种方法，规定范围放置，比如要放置的单词位于青岛，那么就在青岛这个范围内，随便找取一个空闲位置放置，通过实验，这种方法虽然运行速度很快，但由于限定的范围太过宽泛，地理偏差较大\n* 第三种方法，在观察上述两种方法的词云与聚类中心点叠加图层后的结果后受到启发，取了二者的折中，将限定范围缩小，缩小为聚类所占据的范围，为了避免一些聚类边缘点的干扰，首先对聚类的坐标点进行排序，然后取 5%到95% 的数据作为聚类的范围，然后在寻找位置时，在所有可行的坐标中取距离聚类中心点距离最短的一组作为放置位置，**放置算法实现代码如下：**\n\n```python\ndef sample_position_v2(self, size_x, size_y, b_x, b_y, e_x, e_y, center_x, center_y):\n    dis_to_center = 99999999\n    ans_x = 0\n    ans_y = 0\n    for y in range(max(1, b_y), min(e_y - size_y, self.height - size_y)):\n        for x in range(max(1,b_x), min(e_x - size_x,self.width - size_x)):\n            area = self.integral[y - 1, x - 1] + self.integral[y + size_y - 1, x + size_x - 1]\n            area -= self.integral[y - 1, x + size_x - 1] + self.integral[y + size_y - 1, x - 1]\n            if not area:\n                tmpdis = Euclidean_distance(x + size_x/2 , center_x, y + size_y/2, center_y).real\n                if tmpdis \u003c dis_to_center:\n                    dis_to_center = tmpdis\n                    ans_x = x\n                    ans_y = y\n    if dis_to_center == 99999999:\n        return None\n    else:\n        return ans_y, ans_x\n```\n**寻找最佳缩放比例的算法代码实现如下：**\n\n```python\nweight_aver = 0.5\npenalty = 9999999999.9\nscaling_factor = 1.0\n# find the best placement\nb_x = center_x - (box_size[0] + self.margin) // 2\nb_y = center_y - (box_size[1] + self.margin) // 2\nfor s_f in np.arange(0.05,1.00,0.05):\n    tmp_penalty = weight_aver * (1 - s_f)\n    new_x = (int)(center_x - box_size[0] * s_f / 2)\n    new_y = (int)(center_y - box_size[1] * s_f / 2)\n    distance_error = cmath.sqrt((b_x - new_x ) * (b_x - new_x ) + (b_y - new_y) * (b_y - new_y))\n    tmp_penalty = tmp_penalty + (1 - weight_aver) * distance_error / cmath.sqrt(A)\n    if tmp_penalty \u003c= penalty:\n        scaling_factor = s_f\n        penalty = tmp_penalty\nfreq = (int)(freq * scaling_factor)\nque.put(WordCluster(word, center_x, center_y , freq, curword.direction, CurRegion, min_x, max_x, min_y, max_y, curword.idx))\n```\n\n* \u003cfont size=4\u003e**颜色分配**\u003c/font\u003e\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20200805161247364.png)\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20200805161256592.png)\n\n```python\ndef Color_Distribution(self, i):\n    Hue_Spectrum = ['rgb(29, 122, 202)', 'rgb(0, 139, 182)', 'rgb(1, 146, 149)', 'rgb(0, 146, 107)',\n                    'rgb(1, 142, 46)', 'rgb(78, 134, 0)', 'rgb(131, 123, 0)', 'rgb(161, 111, 0)',\n                    'rgb(182, 94, 46)', 'rgb(197, 78, 110)', 'rgb(197, 69, 152)', 'rgb(180, 77, 186)',\n                    'rgb(135, 98, 204)']\n    l = len(Hue_Spectrum)\n    c = 3\n    return Hue_Spectrum[(i + c) % l]\n```\n**放置算法的完整代码如下：**\n\n```python\ndef GeoGenerate(self, words, A , Am ,geomap, cluster_points, PointsNum, max_font_size=None):\n     '''\n     :param words: Preprocessed data sets\n     :param A: the area of the rectangle bounding region M\n     :param Am: the area of M in pixel\n     :param geomap : the region coordinate\n     :param cluster_points : the points that labeled with the cluster index\n     :param max_font_size: Max Font Size\n     :return: Geo Word Cloud\n\n     '''\n     # Coverage error\n     Measure1 = 0.0\n     # Words not represent\n     Measure2 = 0.0\n     # difference between M and M'\n     Measure3 = 0.0\n     MM = np.zeros((800,500))\n     # the total frequency\n     n = 0\n     que = Q.PriorityQueue()\n     min_freq = 999999999\n     for i in range(0, words.shape[0], 1):\n         que.put(WordCluster(words[i][0], words[i][1], words[i][2], words[i][3], words[i][4], words[i][5],words[i][6],words[i][7],words[i][8],words[i][9], i+1))\n         n = n + words[i][3]\n         if min_freq \u003e words[i][3]:\n             min_freq = words[i][3]\n\n     # the number of the cluster\n     CountWord  = que.qsize()\n\n     '''\n     #users can diy the fill word to complete the shape of the map\n     \n     if que.qsize() \u003c self.max_words:\n         for i in range(0, self.max_words - que.qsize(), 1):\n             # users can diy fill words\n             que.put(WordCluster('fill', self.width//2, self.height//2, (int)(min_freq * 0.5) , 1))\n     '''\n\n     frequencies = list()\n\n     if self.random_state is not None:\n         random_state = self.random_state\n     else:\n         random_state = Random()\n\n     # set the mask\n     if self.mask is not None:\n         boolean_mask = self._get_bolean_mask(self.mask)\n         width = self.mask.shape[1]\n         height = self.mask.shape[0]\n     else:\n         boolean_mask = None\n         height, width = self.height, self.width\n\n     occupancy = IntegralOccupancyMap(height, width, boolean_mask)\n\n     # create image\n     img_grey = Image.new(\"L\", (width, height))\n     draw = ImageDraw.Draw(img_grey)\n     img_array = np.asarray(img_grey)\n     font_sizes, positions, orientations, colors = [], [], [], []\n\n     last_freq = 1.\n\n     if max_font_size is None:\n         # if not provided use default font_size\n         max_font_size = self.max_font_size\n\n     curidx = 0\n\n     # start drawing grey image\n     while not que.empty():\n         curword = que.get()\n         freq = curword.num\n         word = curword.word\n         CurRegion = curword.region\n         # the cluster center\n         center_x = (int)(curword.x)\n         center_y = (int)(curword.y)\n         # the cluster range\n         min_x = (int)(curword.min_x)\n         max_x = (int)(curword.max_x)\n         min_y = (int)(curword.min_y)\n         max_y = (int)(curword.max_y)\n\n         # select the font size\n         rs = self.relative_scaling\n\n         if rs != 0:\n             font_size = int((cmath.sqrt((freq * Am)/n).real))\n\n         # select the rotation\n         if curword.direction == 1:\n             orientation = None\n         else:\n             orientation = Image.ROTATE_90\n\n         tried_other_orientation = False\n         #Origin_orientation = orientation\n\n         while True:\n             # try to find a position\n             font = ImageFont.truetype(self.font_path, font_size)\n             # transpose font optionally\n             transposed_font = ImageFont.TransposedFont(\n                 font, orientation=orientation)\n             # get size of resulting text\n             box_size = draw.textsize(word, font=transposed_font)\n             # find possible places using integral image:\n             '''\n             if freq \u003c min_freq:\n                 result = occupancy.sample_position_v2(box_size[0] + self.margin, box_size[1] + self.margin,\n                                                       1, 1,self.width ,self.height )\n             else:\n             '''\n             '''\n             b_x = geomap[CurRegion][0]\n             b_y = geomap[CurRegion][2]\n             e_x = geomap[CurRegion][1]\n             e_y = geomap[CurRegion][3]\n             '''\n\n             result = occupancy.sample_position_v2(box_size[0] + self.margin, box_size[1] + self.margin,\n                                                  min_x, min_y, max_x, max_y,center_x,center_y)\n\n             if result is not None or font_size \u003c self.min_font_size:\n                 # either we found a place or font-size went too small\n                 break\n             # if we didn't find a place, make font smaller\n             # but first try to rotate\n             if not tried_other_orientation :\n                 orientation = (Image.ROTATE_90 if orientation is None else Image.ROTATE_270)\n                 tried_other_orientation = True\n                 continue\n             # scale the word down\n             else:\n                 weight_aver = 0.5\n                 penalty = 9999999999.9\n                 scaling_factor = 1.0\n                 # find the best placement\n                 b_x = center_x - (box_size[0] + self.margin) // 2\n                 b_y = center_y - (box_size[1] + self.margin) // 2\n                 for s_f in np.arange(0.05,1.00,0.05):\n                     tmp_penalty = weight_aver * (1 - s_f)\n                     new_x = (int)(center_x - box_size[0] * s_f / 2)\n                     new_y = (int)(center_y - box_size[1] * s_f / 2)\n                     distance_error = cmath.sqrt((b_x - new_x ) * (b_x - new_x ) + (b_y - new_y) * (b_y - new_y))\n                     tmp_penalty = tmp_penalty + (1 - weight_aver) * distance_error / cmath.sqrt(A)\n                     if tmp_penalty \u003c= penalty:\n                         scaling_factor = s_f\n                         penalty = tmp_penalty\n                 freq = (int)(freq * scaling_factor)\n                 que.put(WordCluster(word, center_x, center_y , freq, curword.direction, CurRegion, min_x, max_x, min_y, max_y, curword.idx))\n                 break\n\n         # can place\n         if result is not None:\n             x, y = np.array(result) + self.margin // 2\n             # actually draw the text\n             draw.text((y, x), word, fill=\"white\", font=transposed_font)\n             tmpmeasure = Hausdorff_Distance(y,x,box_size[0] + self.margin,box_size[1] + self.margin,curword.idx,cluster_points)\n             tmp1, tmp2 = np.array(tmpmeasure)\n             Measure1 = Measure1 + tmp1\n             Measure2 = Measure2 + tmp2\n             Fill_Map(MM, y, x,box_size[0] + self.margin, box_size[1] + self.margin)\n             # append attributes\n             frequencies.append((word, freq))\n             positions.append((x, y))\n             orientations.append(orientation)\n             font_sizes.append(font_size)\n             '''\n             colors.append(self.color_func(word, font_size=font_size,\n                                           position=(x, y),\n                                           orientation=orientation,\n                                           random_state=random_state,\n                                           font_path=self.font_path))\n             '''\n             colors.append(self.Color_Distribution(curidx))\n             curidx = curidx + 1\n             # print(colors)\n             # recompute integral image\n             if self.mask is None:\n                 img_array = np.asarray(img_grey)\n             else:\n                 img_array = np.asarray(img_grey) + boolean_mask\n             # recompute bottom right\n             # the order of the cumsum's is important for speed ?!\n             occupancy.update(img_array, x, y)\n             last_freq = freq\n\n     # layout\n     self.layout_ = list(zip(frequencies, font_sizes, positions,\n                             orientations, colors))\n\n\n     # Three Measures\n     Measure1 = (Measure1/PointsNum).real\n     Measure2 = (PointsNum - Measure2)/PointsNum\n     Measure3 = (162384 - len(MM[MM == 1]))/162384\n     print (\"Measure1:\")\n     print (Measure1)\n     print (\"Measure2:\")\n     print (Measure2)\n     print(\"Measure3:\")\n     print(Measure3)\n\n     return self\n```\n\n### 衡量指标\n* \u003cfont size=4\u003e**Measure 1**\u003c/font\u003e\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20200805163321711.png)\n论文中有这么一句话，我没有理解好\n\" the coverage error is measured as the total sum of distances and we divide this error by the diagonal of the map \" \nthe diagonal of the map 指的是？直接除以对角线的长度还是？\n（我在实现时只除以了点的总数)\n* \u003cfont size=4\u003e**Measure 2**\u003c/font\u003e\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20200805163348611.png)\n*  \u003cfont size=4\u003e**Measure 3**\u003c/font\u003e\n![在这里插入图片描述](https://img-blog.csdnimg.cn/2020080516340778.png)\n\n```python\ndef Euclidean_distance(x1, x2, y1, y2):\n    return cmath.sqrt((x2 - x1) * (x2 - x1)  + (y2 - y1) * (y2 - y1))\n\ndef Hausdorff_Distance(x, y, w, h, idx, cluster_points):\n    sz = len(cluster_points[idx])\n    min_x = x\n    max_x = x + w\n    min_y = y\n    max_y = y + h\n    dis = 0\n    num = 0\n    for i in range(0, sz):\n        cur_x = cluster_points[idx][i][0]\n        cur_y = cluster_points[idx][i][1]\n        if cur_x \u003e= min_x and cur_x \u003c= max_x and cur_y \u003e= min_y and cur_y \u003c= max_y:\n            num = num + 1\n            continue\n        mindis = 99999999\n        for x2 in range(min_x,max_x + 1):\n            tmpdis1 = Euclidean_distance(cur_x, x2, cur_y, min_y).real\n            tmpdis2 = Euclidean_distance(cur_x, x2, cur_y, max_y).real\n            tmpdis = min(tmpdis1, tmpdis2)\n            if mindis \u003e tmpdis:\n                mindis = tmpdis\n        for y2 in range(min_y, max_y + 1):\n            tmpdis1 = Euclidean_distance(cur_x, min_x, cur_y, y2).real\n            tmpdis2 = Euclidean_distance(cur_x, max_x, cur_y, y2).real\n            tmpdis = min(tmpdis1, tmpdis2)\n            if mindis \u003e tmpdis:\n                mindis = tmpdis\n        dis = dis + mindis\n    print((dis,num))\n    return dis,num\n\ndef Fill_Map(MM, x, y, size_x, size_y):\n    for i in range(x - 1, x+size_x):\n        for j in range(y - 1, y+size_y):\n            MM[i][j] = 1\n           \n.\n.\n.\n# After placing one word\ntmpmeasure = Hausdorff_Distance(y,x,box_size[0] + self.margin,box_size[1] + self.margin,curword.idx,cluster_points)\ntmp1, tmp2 = np.array(tmpmeasure)\nMeasure1 = Measure1 + tmp1\nMeasure2 = Measure2 + tmp2\nFill_Map(MM, y, x,box_size[0] + self.margin, box_size[1] + self.margin)\n.\n.\n.\n# At the end of the algorithm \nMeasure1 = (Measure1/PointsNum).real\nMeasure2 = (PointsNum - Measure2)/PointsNum\nMeasure3 = (162384 - len(MM[MM == 1]))/162384\n```\n\n## 实验分析\n数据集大小：10000\n聚类后单词总数：126\n与原论文中的法国数据集大小相似\n\n**Measure1：** 38.479475028111644\n**Measure2：** 0.8894705053352124\n**Measure3：** 0.13974899004828062\n**Total Time（包含了加载数据、词云可视化、指标衡量计算）：** 47.89249 s\n\n**Measure1** 在这里的意义是所有点地理误差的平均值，约为 38 像素，按比例换算为实际距离，代表的意义是所有点地理位置偏差的平均值约为 30 km（虽然从山东省的面积来看 30km 并不大：除以地图的宽/高得，在地图水平方向的偏差为5.6%，在地图竖直方向上的偏差为8.7%），但我个人认为可能与放置算法的关系不是很大，而对其影响最大的是数据的分布以及聚类的结果。\n\n原因如下：\n\n首先从放置算法来看，它的基础操作对象实际是每个聚类，其 best placement 的计算中放置的误差设置的也是基于聚类的中心点，而指标一的计算是基于聚类中的每一个点，计算聚类中所有点到单词矩形框的豪斯多夫距离然后加和，当数据分布较为集中时，大多数点都在矩形框内，指标一的值很小，而当数据分布距离聚类中心点“比较远”时，那么这个加和后的值是非常大的。在原有算法的基础上，要想使指标一的值减小，可以加大 K-means 的 K 值，但这会使聚类数增多，同时原本高频率的大单词可能会因此被分成几个距离较近的小单词（比如不进行聚类的极端情况），可视化的效果较差。\n\n**Measure2** 在这里的意义是没有被矩形框覆盖的点所占的比例约为 88%，通过观察程序的运行结果发现，最先被放置，即频率较大的单词，被覆盖的点的比例是比较大的，但后面的单词，由于大多数空间被前面的大单词所占据，它们要缩小字体大小以完成放置，那么它们被矩形框所覆盖的点的比例是很小的，尤其是数据点比较分散，比较多的时候。\n\n比如单词 Edith 的覆盖基本准确，但由于矩形框较小且数据点的分布比较分散，所以没有被覆盖的点依旧很多。\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20200805150016106.png)\n再来看论文中的这一张原图\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20200805175348718.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDc3MTc1Nw==,size_16,color_FFFFFF,t_70)\n不知道是省略了一些点还是原数据点就这么少。从这张图来看，出现频率最高的三个单词，即最优先被放置的三个单词，它们的数据点被矩形框覆盖的比例也不是很大，比如 左上角的 Tomme、Crottin 几乎没有覆盖对应的任何数据点，原论文的实验结果中没有被覆盖点的比例最小是 4.17 %，要实现这个数值的话，感觉很困难，除非数据分布非常紧密、放置位置非常精确。那么就需要继续改进我们的聚类算法和放置算法。\n\n**Measure 3：** 对称性差异仅为 14%，原文中这一数值的最小值为 22%，从这一指标来看还是比较可观的\n\n**Total Time：** 50 s 左右，因为按自己的理解修改了放置算法，减少了过多的迭代，所以在时间上会远优于原论文的 30 min。\n\n现阶段，我觉得用以下所修正的指标来衡量当前的实现质量可能会更好一些：\n\n**Measure 4 :** 聚类中心点到对应单词矩形框的豪斯多夫距离的中位数\n设置为中位数的原因是为了避免一些 “outliers” 的影响，比如前面较大的单词，放置时空间比较宽阔，聚类中心点在矩形框内的概率很大，其豪斯多夫距离很小，后面较小的单词由于大多数空间已被占据，其豪斯多夫距离很大。\n\n计算结果为：18.384776310850235\n\n各聚类中心到矩形框的豪斯多夫距离如下：\n```bash\n0\n0\n0\n0\n0\n0\n0\n0\n0\n0\n0\n0\n0\n0\n0\n0\n0\n0\n0\n0\n1\n2\n5\n5\n5\n5.38516\n5.65685\n7\n8\n8\n8\n8\n10\n10.6301\n11\n11\n11.7047\n12\n12\n13\n13\n13.4164\n14.3178\n14.4222\n15\n15\n15\n15\n15\n15.6205\n15.6525\n15.8114\n16\n16\n16.6433\n17\n17\n17.0294\n18\n18\n18\n18\n18.3576\n18.3848\n19\n19.2354\n20\n20\n20\n20.3961\n21\n21\n21.0238\n21.587\n22.0227\n22.1359\n22.561\n24.4131\n24.6982\n25.4951\n25.5539\n27\n27\n27.0185\n27.6586\n28\n28.4253\n29\n29.2746\n29.5296\n29.6142\n30\n30.4138\n32\n32.6497\n32.6497\n33.4215\n33.6006\n34\n34.2053\n34.6554\n35\n36.6742\n36.6879\n37.7359\n38\n38.0132\n38.0789\n38.5876\n40.6079\n40.8534\n41.1096\n41.6773\n44.5533\n44.9444\n46\n47.0106\n48.3011\n50\n51.0784\n52.3259\n53.9073\n54\n55\n56.4358\n57.9828\n```\n\n由结果可以看出，前面被放置的聚类中心点大多数都在单词矩形框内，确保了重要单词的地理位置精度。\n\n从整个实现的过程来看，编程实现的难度其实不是很大，重要的是对论文的理解，前两个指标的偏差也可能是由理解的偏差引起的，比如指标一里地图的对角线指的是什么，放置算法里单词缩放后位置的确定以及如何确定覆盖尽可能多的点的位置等等。\n\n## 进一步可能做到的工作\n\n将这个项目完善为一个 Web 端或移动端的自动可视化的应用，通过与用户的交互来提高可视化的质量，比如绘制词云图前首先向用户展示聚类结果，由用户决定是否进行调整，可视化后提供编辑重制功能，为用户提供各单词的地理位置偏移信息、频率信息，然后可以自行调整位置或修改单词的绘制优先级以优化词云的可视化效果等。\n\n## 附件说明\n* GeoWordCloud.py 主程序\n* wordcloud.py 修改了词云库的源码，要测试程序的话只需覆盖原有的库文件即可\n* K_means_Region_Cluster.m 数据预处理\n* PCA_Rotation.m 确定聚类主方向\n* Vis_Output.m 叠加图层观察词云的覆盖情况\n* RandomData.cpp 生成随机数据集\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frussellwzr%2Fgeo_word_clouds","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frussellwzr%2Fgeo_word_clouds","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frussellwzr%2Fgeo_word_clouds/lists"}