☰
基于机器学习的旅游方式偏好预测:以南京市为例的建模实践
2026/10/13 22:42:06 网站建设 项目流程

简介:这份资源是一篇题为《基于机器学习技术的旅游方式偏好研究——以南京市为例》的学术论文PDF,面向旅游管理、机器学习等交叉领域的学者和学生,可作为研究方法与论文写作的参考范例。资源仅含1个PDF文件,压缩包大小474KB,全文内容完整。论文首先梳理了旅游方式偏好的定义与分类,将旅游方式划分为团队游与自助游两类;随后介绍了机器学习技术在该问题中的应用思路,重点阐述了C4.5规则归纳算法的两阶段学习机制。研究基于305名南京居民的问卷调查数据,通过人口统计特征和个人价值观等变量构建预测模型,最终提取出可解释的决策规则,分析了南京市居民旅游方式偏好的主要特点。读者可从中学习问卷设计、数据预处理、算法选型、模型解释等完整流程,也可借鉴其将机器学习应用于社会科学实证研究的思路。目前已有125人学习浏览,适合作为旅游大数据、用户偏好分析等方向的入门参考或参考文献。

1. 基于机器学习技术的旅游方式偏好研究:以南京市为例,到底要解决什么问题

《基于机器学习技术的旅游方式偏好研究——以南京市为例》这类题目,拿到手第一反应往往是:这到底是一篇论文,还是一个推荐系统项目?答案是两者都是。业务上是回答“来南京的游客,到底更认跟团、自由行、半自由行还是定制游”;技术上是把这个问题做成一个可解释的机器学习分类与预测任务。传统统计只能告诉你“收入与是否选择自由行显著相关”,机器学习却能回答更刁钻的问题:一个42岁、带两个老人、第一次来南京、预算充足的游客,最可能选哪种出行方式,以及排第二、第三的是什么。这篇笔记把这类项目的完整落地路径拆开:从问题定义、标签构造、特征工程、模型选型到避坑排查,最后落到可解释输出。

2. 先定义问题:旅游方式偏好是分类、回归还是排序

做机器学习项目的第一步不是调包,是把业务问题翻译成能用损失函数描述的问题。旅游方式偏好这四个字,决定技术路径的是后面两个字:偏好。偏好不等于行为,一个人在问卷里勾“我喜欢自由行”,实际可能因为假期短、带老人,最后选了跟团。所以建模前要回答:预测的是态度,还是行为。

我一般按用途分两种情况。目标是学术研究、讲清楚哪些因素与偏好相关,就做分类建模,把“最常采用的旅游方式”作为标签,做多分类;目标是给游客做产品推荐,就得考虑排序,对每个游客输出一个偏好得分列表,而不是只给一个类别。两个目标对应完全不同的评估方式,前者看F1与混淆矩阵,后者看Top-3命中率与NDCG。动手前先问清楚需求,能省大量返工。

2.1 旅游偏好研究的核心问题:分类、回归还是排序

最常见的落地路径,是把旅游方式偏好定义成单标签多分类。标签就是问卷里那道经典题:“您最常采用的旅游方式是?”,选项通常是跟团游、自由行、半自由行(机票酒店自理、目的地跟团)、定制游、自驾游。这个做法优点是标签来自问卷回收,不需要额外标注,复现门槛低;缺点是丢信息,一个游客可能周末自驾、长假自由行,“最常采用”逼他只能选一个。

另一种是多标签分类,把“过去一年实际采用过哪些旅游方式”作为标签。它能还原多重偏好,但标签之间强相关,评估时多标签指标在业务汇报里很难讲清楚。我的建议是按项目周期来:课程设计、毕业论文或快速验证,优先单标签多分类;真要做个性化推荐,再上多标签或排序模型。

还有一种少见但值得提的方案:不做分类做回归,对每种旅游方式的偏好程度分别打分。好处是能输出“自由行3.2分、跟团2.1分”这种细粒度结果;坏处是问卷打分题容易整体偏高,模型要额外处理个体打分膨胀,而且要维护多个模型,成本和复杂度都上去了。三种方案在同一个南京问卷上的取舍,用下面这张表表达:

建模方案标签形式典型用途代价
单标签多分类最常采用的方式论文、画像、预案丢掉多重偏好信息
多标签分类过去一年用过的方式个性化推荐评估复杂、汇报难懂
回归打分对每种方式的偏好分精细化推荐打分膨胀、模型多

2.2 标签怎么定义:从问卷选项到监督信号

定好方案后,第一件事不是跑模型,而是看标签分布。以南京市为主的游客问卷为例,“过去一年最主要的旅游方式”这道题,自由行通常占一半以上,跟团和定制游各占两成左右,自驾看样本来源。如果自由行占60%,全猜自由行的模型准确率就已经是60%,看着不差,实际对跟团和定制游完全没有区分力。先把分布打印出来看,是建模前的基本功。

标签合并是第二步。有些选项样本量太小,比如“邮轮游”一年就几十条,直接删掉浪费,硬学又学不动。我一般把占比低于5%的类别向语义最近的类别合并:半自由行与小包团合并,邮轮游向跟团游合并;合并后仍然撑不起一个类别的,就作为“其他”剔除,或交给第4章的不平衡处理兜底。标签是整个监督信号的质量上限,这一步值得反复试几次再定稿。

还有一个容易被忽略的选项:“说不清/未决定”。很多问卷会设这个选项,我的处理习惯是保留它作为一个独立类别,不是直接删掉。因为它在真实业务里恰恰是推荐最该发力的对象——明确要自由行的人不需要推荐,“说不清”的人才是潜在转化人群。如果这个选项占比超过30%,问题通常不在建模,而在问卷的旅游方式选项和游客心智不匹配,优先回头改问卷。

2.3 评估方式:为什么准确率不是第一指标

分类任务的默认指标是准确率,但在这个题里准确率是陷阱,原因上一节已经讲过:类别不平衡。我固定看三件套。第一是宏平均F1,它给稀有类别相同权重,能反映少数类的真实表现;第二是混淆矩阵,直接看跟团游客被分去了哪个错误类别,比如“跟团被错分为半自由行”可能意味着两条产品线在模型视野里分不开;第三是Cohen's Kappa,它修正了随机猜对的成分,论文里写“Kappa=0.42,中等一致”比写“准确率85%”更稳。

如果项目要落到旅游产品推荐,我会加算Top-3命中率:模型输出的前三个候选里,是否包含用户真实采用的旅游方式。这个指标业务方一听就懂,而且比F1更贴近实际使用方式——用户本来就不只会选一种出行方式。指标要在建模前定好,不要等模型训练完再挑一个最好看的,这是机器学习应用流程里最容易造成返工的一步。

3. 特征体系与工程化:数据源、编码和衍生特征

标签定义好之后,决定模型上限的是特征。旅游偏好这个题目,特征来源通常有三个:问卷的字段化答案(年龄、收入、同行人数)、游客在平台上的行为日志(浏览过的景点、收藏过的线路)、目的地本身的属性数据。以南京市为例的区域研究,第三个来源尤其重要,同样是“想来南京”,想看博物馆的人和想看夜游的人,偏好的旅游方式大概率不同。

3.1 数据源与字段设计:问卷为主、行为数据为辅

论文和课程设计项目,绝大多数情况只有问卷数据,这不算硬伤,但要把问卷字段当特征工程来做,而不是当统计表来做。我一般在拿到问卷后按四组整理。

第一组是人口属性:性别、年龄、学历、常住城市等级、年收入。常住城市等级是比省份更强的特征,一线城市游客对自由行的接受度通常显著更高,但它不能直接做整数编码,要换成城市分级。

第二组是出行特征:同行结构(独自、情侣、家庭带娃、陪伴老人、朋友结伴)、出行天数、人均预算、出游季节偏好。这组和标签的关系最直接,家庭带娃和陪伴老人的偏好曲线几乎是镜像的。

第三组是信息渠道:从哪知道南京的,OTA、短视频、朋友推荐或旅行社门店。这组能部分反映决策习惯,短视频渠道人群通常更吃网红化半自由行产品。

第四组是目的地兴趣:对博物馆、历史街区、自然风光、演艺演出是否感兴趣。做以南京市为例的项目,这组字段是体现区域特色的核心。

行为日志数据如果有,优先级反而要高过人口属性。浏览过定制游详情页、搜索过“南京三日游攻略”,这接近真实意图;问卷里的“我喜欢自由行”只是态度。但行为数据有两个麻烦:一是时间窗口,游客可能浏览十次半自由行再下单自由行,窗口截错了标签就学错;二是冷启动用户没有行为记录,2.2节说的“说不清”人群很容易同时是无行为人群。

3.2 特征编码的实操清单

下面是一张我从南京游客问卷里整理出的常见字段表,类型和处理方式都标注清楚:

特征名含义类型取值示例处理方式
age年龄连续36取对数后标准化
city_tier常居城市等级有序一线/二线/其他顺序编码0/1/2
companion同行结构类别家庭带娃/陪伴老人/朋友独热或目标编码
budget_per_person人均预算连续2500元取对数后标准化
info_source信息渠道类别OTA/短视频/门店独热编码
poi_interest兴趣标签多值类别博物馆/夜游多值拆列
travel_days出行天数连续4标准化

具体编码有几个讲究。年龄和预算都有长尾分布,先取对数再标准化,比直接标准化稳得多。城市等级是有序变量,用0/1/2顺序编码而不是独热,因为一线到二线本身有递进关系。兴趣标签是多选题,不要拆成多列后全部独热,我一般只保留出现率超过10%的标签,其余归为低频兴趣,否则特征维度直接爆炸。拆列的代码很简单:

# 把分号分隔的多选兴趣字段拆成 0/1 矩阵 from sklearn.preprocessing import MultiLabelBinarizer df['poi_list'] = df['poi_interest'].str.split(';') mlb = MultiLabelBinarizer() poi_mat = mlb.fit_transform(df['poi_list']) poi_df = pd.DataFrame(poi_mat, columns=mlb.classes_) # 保留出现率超过 10% 的兴趣列,低频合并成一列 freq_cols = poi_df.columns[poi_df.mean() > 0.1] low_freq_cols = poi_df.columns[poi_df.mean() <= 0.1] poi_df['低频兴趣'] = poi_df[low_freq_cols].max(axis=1) poi_df = poi_df[list(freq_cols) + ['低频兴趣']]

代码先把一个包含多个兴趣标签的字段拆成稀疏矩阵,然后算每列的出现率。出现率阈值10%是为了防止极低频标签变成噪声列;低频兴趣归并列用按行取最大值,表示这个人是否至少勾了一个低频标签。这套处理的目的是把“有多少个兴趣、是哪些兴趣”变成稳定特征,而不是让100个不相关标签都进模型。

3.3 三个值得手动做的衍生特征

特征工程的高性价比部分在衍生交叉。我在这类项目里一定会做下面三个。

第一个是同行结构乘人均预算交叉。带娃家庭预算弹性最大,情侣对人均预算的敏感度反而低于预期,这两个变量单独建模看不出交互,做成交叉特征后效果一目了然。具体做法是把同行结构做标签编码,与对数化预算相乘,得到一个连续特征。

第二个是信息渠道乘年龄。短视频渠道的特征随年龄衰减极快,OTA渠道则平缓。这个交互能识别出年龄大但爱看短视频的高知游客,他们可能是定制游的高潜人群。这类信息在数据里存在,但单特征学不到。

第三个是目的地兴趣组合。同时勾了博物馆和历史街区的游客,与同时勾了夜游和网红餐饮的游客,预测的旅游方式可能完全相反。把兴趣标签做成两两组合的计数值,能释放这部分信息,论文项目用计数组合就够,别把嵌入方案搞得太重。

特征工程做完,一定要做一次反向验证:随机抽200条样本,把特征摊开人工看一遍。这步虽然土,但能发现大量编码错误,比如把没收入的学生填的0当异常值删掉,或者把“其他”选项做成一个特征维度,结果模型学到的全是选了其他的人更爱跟团这种抽样噪声。

4. 模型选型与训练:从机器学习应用流程里的基线到类别不平衡处理

特征就位,进入建模。一个机器学习项目最忌讳一上来就上XGBoost和深度学习,旅游方式偏好这个题目的样本量通常在几百到几千,树模型和线性模型比深度网络靠谱得多;而且项目要出可解释结论,深度网络在论文里很难自圆其说。我的建模顺序很固定:先跑逻辑回归对齐预期,再上随机森林或梯度提升提效果,最后根据类别分布决定要不要做重采样。

4.1 基线模型:用逻辑回归先对齐预期

逻辑回归在这个业务里不是最终模型,它是一把校准尺。跑完它能回答三个问题:数据里是否真的有信号、特征量纲和编码是否有问题、业务直觉是否和数据吻合。比如逻辑回归跑完,如果家庭带娃这个特征在自由行类别上的系数显著为负,说明相比其他方式,带娃家庭确实更少倾向自由行,这和常识一致,流程基本没跑偏;如果这个系数方向反了,先查特征编码再查数据集,往往能找到脏数据。

import pandas as pd from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.compose import ColumnTransformer from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report, f1_score, cohen_kappa_score # 连续特征已做过取对数,离散特征走独热 num_cols = ['age_log', 'income_log', 'travel_days', 'group_size', 'budget_log', 'poi_interest_cnt'] cat_cols = ['gender', 'city_tier', 'companion', 'info_source', 'season_pref', 'is_nanjing_repeat'] X = df[num_cols + cat_cols] y = df['travel_mode'] # 0=跟团 1=自由行 2=半自由行 3=定制游 preprocessor = ColumnTransformer( transformers=[ ('num', StandardScaler(), num_cols), ('cat', OneHotEncoder(handle_unknown='ignore', max_categories=20), cat_cols) ]) X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, stratify=y, random_state=42) X_train_pre = preprocessor.fit_transform(X_train) X_test_pre = preprocessor.transform(X_test) lr = LogisticRegression(max_iter=1000, class_weight='balanced') lr.fit(X_train_pre, y_train) y_pred = lr.predict(X_test_pre) print(classification_report(y_test, y_pred)) print('macro F1:', f1_score(y_test, y_pred, average='macro')) print('Kappa:', cohen_kappa_score(y_test, y_pred))

代码里三个参数值得单独说明。class_weight='balanced'是在样本不平衡时让少数类获得更高的错分代价,在旅游方式偏好里几乎必开。train_test_split里的stratify=y保证训练集和验证集里各类别比例一致,不开它,跟团游这个少数类可能被随机切走大半。OneHotEncoder里的max_categories=20,是防止常居城市这类高基数类别把特征矩阵撑得太宽。逻辑回归如果报未收敛警告,把max_iter提到1000以上再跑。

4.2 提升模型:随机森林与梯度提升的取舍

逻辑回归跑通后,看宏平均F1,我一般在0.35到0.55之间,这是正常区间。旅游方式偏好本身有噪声,同一个游客今天答和明天答都可能不同,别指望0.9。接下来试树模型。随机森林的优点是不用做太多特征变换、对离散特征和缺失值宽容、训练快;梯度提升的优势是对非线性交互拟合更细,但调参维度更多、小样本下更容易过拟合。我的取舍原则:样本量少于2000用随机森林,多于5000用LightGBM,中间两者都跑,选验证集宏平均F1高且和逻辑回归差距不过大的那个。

from imblearn.over_sampling import SMOTE from sklearn.ensemble import RandomForestClassifier # 承接上一段:preprocessor 已在训练集上 fit,测试集从未参与 # 直接复用上一段的 X_train_pre 与 X_test_pre,避免二次 fit sm = SMOTE(random_state=42, k_neighbors=5) X_res, y_res = sm.fit_resample(X_train_pre, y_train) # 控制深度和叶子大小,防止小样本过拟合 rf = RandomForestClassifier( n_estimators=300, max_depth=8, min_samples_leaf=20, class_weight='balanced_subsample', random_state=42, n_jobs=-1 ) rf.fit(X_res, y_res) y_pred_rf = rf.predict(X_test_pre) print(classification_report(y_test, y_pred_rf)) print('macro F1:', f1_score(y_test, y_pred_rf, average='macro'))

这版代码有两个关键边界。第一,SMOTE只在训练集上做,测试集保持真实分布;如果把所有数据一起重采样再切分,验证集里会出现合成样本,评估结果虚高,这是第5章会展开的数据泄漏的一种。第二,随机森林的max_depth限制在8、min_samples_leaf设20,配合class_weight='balanced_subsample',是小样本防止过拟合的经典组合;如果换LightGBM,对应参数是max_depth和min_child_samples,作用类似。

提示:树模型不需要特征标准化,逻辑回归需要。前面用ColumnTransformer把两类特征分开处理,就是为了一次预处理同时喂给两类模型。

4.3 类别不平衡与SMOTE的边界

类别不平衡是这个题目的常态。上一节的SMOTE是过采样手段,但它有两个副作用:合成样本可能不符合业务逻辑,比如把带娃家庭的近邻插值生成一个独自带三娃的样本;以及训练时间变长。如果不想做SMOTE,另一个方案是直接调高少数类权重,逻辑回归的class_weight、随机森林的balanced_subsample都在做这件事。我的习惯是都试,比较验证集宏平均F1,谁高用谁;但决策时一定同时看混淆矩阵,因为有些重采样方案把跟团游的F1从0.2拉到0.6,代价是自由行从0.8掉到0.6。这种拆东墙补西墙,要看业务上哪个类别更重要。

还要注意一种情况:如果某个少数类样本量少于50条,SMOTE强造样本会让模型学到的规律更不稳,此时宁可用降权加简单模型,也别硬造数据。类别少到一定程度,模型的泛化能力天花板不在算法,在标签的真实样本量。

如果项目不是发论文而是业务落地,模型输出不要只有类别,还要有概率。同样判成自由行,概率0.53和0.91的置信度完全不同。把模型改成predict_proba输出Top-3,是后续做推荐列表和人工复核的基础。这一步在代码里只是一行,在产品里是两种体验。

最后是验证方式。小数据集上单次切分的指标波动很大,同一份数据换个随机种子,宏平均F1能差0.05以上。我一般用5折交叉验证,取五折宏平均F1的均值和标准差,标准差超过0.03就说明模型不稳定,优先回头修数据,而不是继续调参。

5. 旅游偏好建模常见问题排查:五个高频翻车点

这类项目翻车往往不在模型,而在数据定义和验证过程。下面五条是我自己做下来见过最多的问题,每条按现象、原因、解决展开,可以直接当排查清单用。

5.1 数据泄漏:问卷里的送分题

现象:模型在验证集上宏平均F1高达0.85,比逻辑回归的合理区间高出一大截,特征重要性前几名全是某个偏好相关字段。

原因:问卷里有一道题,例如“您倾向于选择哪种旅游方式”,它在问卷里的物理位置距离标签题很近,本质上已经包含了答案;或者行为日志里用了游客最终下单的线路类型去构造特征,等于用未来预测现在。还有一种隐蔽泄漏我常踩:做数据清洗时用全样本均值填充缺失值,验证集的信息在训练前就混进去了。

解决:把问卷里所有和方式选择直接同义的题目从特征里移除,只保留前置因素(人口、同行、预算、信息渠道、兴趣)。行为日志按时间窗口切分,只允许用访问发生在标签记录之前的特征。缺失值填充必须在train_test_split之后,在训练集上fit、在测试集上transform。

5.2 样本偏差:南京样本不代表全量游客

现象:模型对45岁以上游客的预测准确率明显低于年轻人,但总体准确率看起来没问题。

原因:问卷回收渠道偏线上,样本里20到35岁占七成,45岁以上样本少,模型学不到这个群体的规律。以南京市为例的区域研究尤其要注意:暑期到南京的亲子游客和春季商务客人构成完全不同,单次采集的数据只代表采集时段和渠道。

解决:第一,分层抽样,按年龄组、出游时段保持和业务目标人群一致的比例,样本不足的组宁可在建模时用小权重也不硬凑;第二,模型评估按人群分组报告,每组单独算宏平均F1;第三,如果要做时段外推,明确标注模型适用范围,别把五一期间南京游客的结论写成南京游客。

5.3 类别不平衡:高准确率是假象

现象:分类报告里整体准确率0.78,但看细分,跟团游的召回率只有0.12,几乎所有跟团游客都被分成了自由行。

原因:训练集里自由行占60%以上,模型发现全猜自由行就能拿到高准确率,少数类几乎没有梯度信号。这是旅游方式偏好数据里最普遍的问题,不是bug,是监督信号的天然分布。

解决:用宏平均F1替代准确率作为主指标,必要时加SMOTE或class_weight重加权;把类别分布写进报告,准确率相同、分布不同,结论完全不同。还有一条容易被忽略的:预测时不要把概率阈值固定在0.5,按验证集P-R曲线找各类别的平衡点,对少数类可以主动下调判定阈值。

注意:调阈值只能看验证集,不要用测试集来调,否则又变成一种数据泄漏。

5.4 特征相关性伪装成因果偏好

现象:特征重要性排名里年收入排第一,报告写了一句高收入人群更偏好自由行,业务方质疑:他们的高净值客户明明更爱定制游。

原因:模型看到的是相关性,不是因果。高收入群体同时也是高学历、一线城市、更早接触在线旅游的一批人,年收入只是这批人行为的代理变量。还有一种更隐蔽的:样本采集渠道本身就偏向某一收入层,导致该特征和标签的关系是采样机制造成的,不是真实世界的偏好。

解决:第一,不要对单个特征的系数下因果结论,用SHAP看交互,可能发现高收入在一线城市里倾向自由行,在二线城市里倾向定制游;第二,报告里写“与自由行偏好相关的特征中,年收入贡献度最高”,不写“高收入更偏好自由行”;第三,在结论部分把预测和归因分开陈述。

5.5 分布漂移:换了个季节模型就不灵了

现象:上半年训练的模型在暑期数据上整体指标掉了0.1,主要错在带娃家庭这一类。

原因:游客构成随时间变化。4月南京的游客以商务和老年团为主,暑假以亲子家庭为主,同行结构、预算、偏好的联合分布整个变了。模型没有适应这种时间变化,训练时也没见过暑期模式。

解决:没有一劳永逸的办法,我一般做两件事。一是给模型加采集季节这一特征,让树模型有机会学出季节条件分支;二是建立监控,每次新数据进来后和训练集的PSI对比,PSI超过0.25就触发重训。如果项目周期短,至少要在论文里标注数据采集月份,别让模型背季节不对的锅。

6. 让结论被采纳:可解释输出与验证习惯

模型准确率再高,旅游偏好研究最终要面对的是“凭什么这样说”的问题。我会在交付时固定附上两张图。

6.1 SHAP摘要图:把谁偏好什么说清楚

用SHAP对随机森林或梯度提升模型做解释,能得到两类输出:全局的特征贡献排序,和单样本的预测分解。全局图回答哪些因素对旅游方式偏好影响最大,单样本图回答这个35岁带娃的杭州妈妈为什么被预测为半自由行。在论文里,SHAP摘要图比一张特征重要性柱状图更有说服力,因为它画出了特征高值低值分别把预测推向哪个方向。以南京数据为例,同行结构为家庭带娃的高贡献和人均预算高贡献常常同时出现在半自由行的预测里,单看其中一个都会得出偏颇结论。

6.2 把预测输出成Top-3偏好候选

业务交付时我一般不输出单一预测类别,而是用predict_proba排序后输出Top-3。比如输出“最可能半自由行(0.52),次选自由行(0.28),再选跟团小包(0.12)”。原因是旅游产品推荐天然允许给用户多个候选,把硬分类变成柔性列表,既保留置信度信息,也给人工作业留了介入空间。这个习惯反过来也约束建模:如果模型只有predict没有predict_proba,或者输出概率在多个样本间几乎没有区分度,说明模型欠拟合,需要回去看特征。

我现在做这类偏好项目,交付固定三件套:混淆矩阵、SHAP摘要图、按人群分层的指标表。少了任何一件,评审会上的提问都能把这套方案问穿:混淆矩阵看模型在哪类上错,SHAP看结论能不能讲成业务故事,分层表看结论经不经得起换个群体还成立吗的追问。做了几个旅游偏好模型之后,我最大的教训是别花太多时间在准确率那零点几个百分点上,真正值钱的是让每个结论都能找到数据里可追溯的痕迹。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询