本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:“上海餐饮数据.zip.zip”是一个涵盖上海市餐饮业多维度信息的综合性数据集,包含餐馆基本信息、菜品价格、用户评价、营业时间、地理坐标、消费档次、菜系分类及交易行为等数据。该数据以压缩格式存储,可能包含CSV、Excel或JSON等多种结构化文件,适用于市场研究、商业智能与学术分析。通过描述性统计、聚类分析、关联规则挖掘和时间序列预测等方法,结合Python、Tableau等工具进行可视化与建模,本项目旨在深入挖掘餐饮市场规律,洞察消费者行为,辅助企业优化运营策略,并为城市餐饮业态研究提供数据支持。

上海餐饮数据的深度挖掘与商业洞察

你有没有想过,为什么某些餐厅门口总是排着长队,而有些同样装修精致的地方却门可罗雀?在一座拥有2500万人口、超16万家餐馆的城市里——上海,答案从来不是“味道好”三个字就能概括的。这背后藏着海量数据之间的隐秘关联:地理位置如何影响评分分布?一条差评真的能让一家店“社死”吗?火锅配啤酒是偶然还是必然?

我们手头这份“上海餐饮数据集”,乍看只是冷冰冰的CSV和JSON文件,但当你真正潜入其中,会发现它像一张巨大的神经网络,连接着消费心理、空间逻辑与时间节奏。今天,咱们就一起拆解这张网,用Python做手术刀,一层层剖开它的肌理。


数据长什么样?别急,先看看它的骨架

打开 上海餐饮数据.zip ,你会发现三张核心表:

  • restaurants.csv :餐馆的基本档案卡
  • reviews.json :用户的喜怒哀乐实录
  • menu.xlsx :每道菜背后的利润密码

它们通过一个共同字段—— restaurant_id ——像齿轮一样咬合在一起,形成典型的星型结构。这种设计很聪明,既避免了重复存储,又方便多维分析。

import pandas as pd

# 加载数据
rest_df = pd.read_csv("restaurants.csv")
review_df = pd.read_json("reviews.json")
menu_df = pd.read_excel("menu.xlsx")

print(f"餐馆数:{len(rest_df)} | 评论数:{len(review_df)} | 菜品数:{len(menu_df)}")

输出可能类似:

餐馆数:8743 | 评论数:126,982 | 菜品数:210,345

哇,快13万条评论!平均每家店有近15条反馈。不过等等……是不是所有数据都靠谱?比如那个写着“人均消费¥0”的米其林候选餐厅?还有经纬度为(0,0)的“幽灵门店”?这些可不是小瑕疵,而是足以让整个模型跑偏的“毒药”。

所以问题来了: 你怎么判断一份数据到底值不值得信?


数据质量评估:别让“脏数据”毁了你的结论

业内有个经典说法:“Garbage in, garbage out.”(垃圾进,垃圾出)。哪怕你用的是BERT或Transformer,输入的是错乱标签,输出也只能是漂亮的废话。

我们得建立一套评估体系。这里推荐一个三维框架: 完整性、一致性、准确性

先来查查“缺胳膊少腿”的情况

missing_ratio = rest_df.isnull().mean() * 100
missing_ratio[missing_ratio > 0].sort_values(ascending=False)

假设结果如下:

avg_cost       12.7%
open_hours     10.3%
cuisine_type    6.8%
rating          3.2%
location        1.5%

看到没,“人均消费”居然有超过一成的数据缺失!而且注意,这类缺失很可能不是随机的——高端餐厅更不愿意标价,这是一种典型的 非随机缺失(MNAR) 。如果你直接拿均值去填,等于把一家本该人均800的私房菜强行拉低到200,这合理吗?

再看 open_hours ,格式五花八门:“10:00-22:00”、“每天营业”、“周一休息”、“见大众点评”。这种非结构化文本,机器根本没法处理。

还有坐标异常点。有人图省事,把未知位置全设成(0,0),导致地图上出现诡异的“黑洞聚集区”。要是你拿这个做热力图,黄浦江边上突然冒出一堆餐馆,那可就闹笑话了。

所以,在动手清洗之前,得先问自己几个灵魂拷问:

  • 缺失是有规律的吗?
  • 异常值是录入错误,还是真实存在的极端案例?
  • 文本字段是否需要统一编码规范?

只有搞清楚这些问题,才能决定下一步怎么走。


清洗实战:从“脏乱差”到“干净整洁”

数据清洗听起来枯燥,但它就像炒菜前的备料——葱姜蒜切好、肉片腌制到位,后面大火快炒才不会翻车。

缺失值处理:别只会填均值!

说到填补空缺,很多人第一反应就是 .fillna(df['col'].mean()) 。但兄弟,时代变了!

根据Rubin的经典理论,缺失机制分三种:

类型 特点 处理建议
MCAR(完全随机) 和任何变量无关 均值/众数填充OK
MAR(随机依赖) 可由其他变量预测 回归/KNN填充更优
MNAR(自身决定) 本身就说明问题 慎重!需建模解释

比如“人均消费”在高端餐厅中普遍为空,这就是典型的MNAR。这时候你应该思考:能不能用评分、评论数量、菜系类型这些信息来预测它?

Scikit-learn里的 IterativeImputer 就是干这个的:

from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer

# 挑选相关特征
features = ['rating', 'review_count', 'cuisine_encoded']
imputer = IterativeImputer(max_iter=10, random_state=42)
rest_df[features] = imputer.fit_transform(rest_df[features])

它像个侦探,反复推理各个变量之间的关系,直到推断出最合理的数值。虽然慢一点,但比瞎猜靠谱多了。

而对于分类变量如 cuisine_type ,可以用众数填充,但要小心扭曲分布。如果某类菜系本身占比就不高,硬塞进去反而会造成偏差。


异常值检测:哪些“离谱”其实是真相?

什么叫异常?有人觉得人均10万元肯定不对;但也有可能,那是外滩某家私人会所的真实定价。

所以我们不能一刀切地删掉,而要学会区分“噪声”和“信号”。

常用的工具有三个:

  1. IQR法 (箱线图规则):适合偏态分布
  2. Z-score :要求接近正态分布
  3. 孤立森林 (Isolation Forest):高维稀疏数据之王

来看一段实战代码:

from sklearn.ensemble import IsolationForest
import numpy as np

cost = rest_df['avg_cost'].dropna().values.reshape(-1, 1)

iso_forest = IsolationForest(contamination=0.05, random_state=42)
preds = iso_forest.fit_predict(cost)
outliers = cost[preds == -1]

print(f"孤立森林标记了 {len(outliers)} 个异常点")

contamination=0.05 意思是:“我预估大约5%的数据是异常的”,相当于给算法一个心理预期。

但重点来了: 检测完之后干嘛?

直接删除?不行。应该先人工审核!

flowchart LR
    Start[开始检测] --> Detect[运行IQR/Z/IF检测]
    Detect --> Count{异常数量 < 阈值?}
    Count -- Yes --> Review[人工审核候选列表]
    Count -- No --> Alert[触发警报通知负责人]
    Review --> Judge{是否合理?}
    Judge -- 是 --> Keep[保留原始值]
    Judge -- 否 --> Fix[修正或删除]
    Fix --> Log[记录日志]
    Keep --> Log
    Log --> End[更新数据集]

比如一家日料店人均980元,听着吓人,但人家卖的是蓝鳍金枪鱼大腹,真有可能。而一家沙县小吃显示人均500块?多半是单位搞错了(把“角”当“元”输进去了),就得纠正。

记住一句话: 数据清洗不是追求完美无瑕,而是确保逻辑自洽。


文本标准化:让机器也能读懂“好吃得飞起”

餐馆名字、地址、营业时间……这些都是文本字段,乱得很。有的写“麦当劳(南京东路店)”,有的写“麦當勞 南京东路”,还有的夹杂emoji 🍔🍟。

我们得统一标准:

def clean_text(text):
    if pd.isna(text):
        return text
    # 转半角字符
    text = unicodedata.normalize('NFKC', text)
    # 去除多余空格
    text = ' '.join(text.split())
    return text.strip()

rest_df['name'] = rest_df['name'].apply(clean_text).str.title()

顺带提一句, str.title() 能把“kfc”变成“Kfc”,虽然不够完美(理想是“KFC”),但至少比全小写强。

至于营业时间,原始数据可能是这样:

  • "10:00-22:00"
  • "休息"
  • "周一至周五 11:00-21:00"

我们需要把它转成可用的时间区间:

from datetime import time

def parse_hours(s):
    if not s or '休息' in s:
        return None, None
    try:
        start, end = s.strip().split('-')
        return time.fromisoformat(start), time.fromisoformat(end)
    except:
        return None, None

rest_df[['open_time', 'close_time']] = pd.DataFrame(
    rest_df['business_hours'].apply(parse_hours).tolist()
)

有了这两个字段,就能进一步计算每日营业时长:

rest_df['duration_hrs'] = (
    pd.to_timedelta(rest_df['close_time'].astype(str)) -
    pd.to_timedelta(rest_df['open_time'].astype(str))
).dt.total_seconds() / 3600

现在你知道哪家店熬得最晚了吧?夜猫子食堂了解一下 😴


特征工程:别只盯着原始字段,要学会“造新零件”

清洗完的数据只是原材料,真正的战斗力来自 特征构造

加权评分:别再被“五星好评”忽悠了!

你有没有发现,很多新开业的小店动不动就4.9分,而老字号反而只有4.3?这是因为评分系统有个致命缺陷: 样本量越小,波动越大

解决办法是什么?引入贝叶斯加权!

参考IMDb的公式:

$$
\text{Bayesian Score} = \frac{R \cdot v + C \cdot m}{v + m}
$$

其中:
- $ R $:该餐厅的平均评分
- $ v $:评论数量
- $ C $:全局平均评分
- $ m $:最小评论阈值(比如取90%分位数)

实现起来很简单:

C = rest_df['rating'].mean()
m = rest_df['review_count'].quantile(0.9)

rest_df['bayesian_score'] = (
    (rest_df['rating'] * rest_df['review_count']) +
    (C * m)
) / (rest_df['review_count'] + m)

这样一来,那些只有两三个好评的“虚假繁荣”就被拉下来了。真正经得起考验的高分选手才会浮出水面。


空间连接:每家店到底属于哪个区?

光有经纬度还不够,我们要知道这家店是在静安寺还是陆家嘴。

这就需要用到GeoPandas了:

import geopandas as gpd
from shapely.geometry import Point

# 加载行政区划边界
districts = gpd.read_file("shanghai_districts.shp")

# 构造餐馆点位
geometry = [Point(xy) for xy in zip(rest_df['lng'], rest_df['lat'])]
gdf = gpd.GeoDataFrame(rest_df, geometry=geometry, crs="EPSG:4326")

# 执行空间连接
joined = gpd.sjoin(gdf, districts, how='left', predicate='within')

从此以后,每家店都有了明确的行政归属。你可以轻松回答这些问题:

  • 哪个区的人均消费最高?
  • 浦东新区 vs 徐汇区,谁的餐饮密度更大?
  • 黄浦区的平均评分是不是显著高于郊区?

这才是真正的“属地化运营”基础。


用户评价分析:数字背后的喜怒哀乐

评分只是一个数字,但用户写的评论才是情绪的真实流露。

评分分布偏了吗?用偏度说话!

画个直方图看看:

sns.histplot(rest_df['rating'], bins=30, kde=True)
plt.title("上海餐馆评分分布")
plt.xlabel("平均评分(满分5)")
plt.ylabel("频次")
plt.show()

print(f"偏度:{skew(rest_df['rating']):.3f}")

如果输出是 -1.874 ,说明严重左偏——绝大多数餐厅集中在4.0以上,低于3.5的基本看不见。

这意味着什么?

👉 不满的顾客更倾向于沉默,而不是打低分。
👉 高分泛滥导致区分度下降,4.5分已经不算特别优秀了。

所以在做组间比较时,别急着用t检验。先做个Shapiro-Wilk正态性检验:

from scipy.stats import shapiro
stat, p = shapiro(rest_df['rating'].dropna())
if p < 0.05:
    print("拒绝正态假设 → 改用Mann-Whitney U检验")

否则你的统计结论可能会翻车。


情感分析:从“不错”到“差劲透顶”,机器能懂吗?

中文情感分析是个 tricky 的活儿。光靠关键词匹配容易误判,比如:

  • “服务还不错,就是价格贵了点” → 实际倾向中性偏负
  • “真是‘惊喜’,厕所都在厨房里” → 明明是讽刺,却被当成正面词

怎么办?两条路:

方法一:词典+规则(轻量级)

用BosonNLP或知网HowNet的情感词典,加上否定词、程度副词的规则处理:

def calc_sentiment(text):
    words = jieba.lcut(text)
    score = 0
    negations = ['不', '没', '无']
    intensifiers = {'非常': 2.0, '很': 1.5, '太': 1.8}

    for i, w in enumerate(words):
        base = sentiment_dict.get(w, 0)
        if i > 0 and words[i-1] in negations:
            base *= -1
        if i < len(words)-1 and words[i+1] in intensifiers:
            base *= intensifiers[words[i+1]]
        score += base
    return score

优点是快、透明;缺点是覆盖有限,遇到新词就歇菜。

方法二:BERT微调(精准但重)

HuggingFace的 bert-base-chinese 模型,经过少量标注数据微调,就能理解上下文语义:

from transformers import BertTokenizer, BertForSequenceClassification

tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')
model = BertForSequenceClassification.from_pretrained('bert-base-chinese', num_labels=3)

# 训练过程略,需准备标注数据

虽然训练费时间,但它能准确识别转折句、反讽句,效果远胜规则法。

📌 建议策略:前期用词典法快速跑通流程,后期用BERT提升精度。


空间热点探测:哪里才是真正“吃货天堂”?

你以为餐馆多的地方就是美食中心?不一定。关键要看是不是形成了 高质量聚集区

核密度估计(KDE):一眼看出“密度洼地”

import seaborn as sns
sns.kdeplot(data=rest_df, x='lng', y='lat', fill=True, cmap="Reds")
plt.title("上海餐馆核密度热力图")
plt.show()

颜色越深,表示单位面积内餐馆越多。你会发现几个明显的红色高亮区:人民广场、徐家汇、中山公园……

但这只是数量上的密集,不代表质量高。

Getis-Ord Gi*指数:找寻“高评分集群”

这才是高手玩法。它不仅能告诉你哪里评分高,还能判断是不是被一群同样高水平的邻居包围着。

from esda.getisord import G_Local
from libpysal.weights import DistanceBand

w = DistanceBand.from_dataframe(gdf, threshold=0.01)  # 1公里内为邻域
g_local = G_Local(gdf['bayesian_score'], w, permutations=999)

gdf['hotspot'] = np.where(g_local.Zs > 1.96, 'Hot Spot',
               np.where(g_local.Zs < -1.96, 'Cold Spot', 'Not Significant'))

结果可以分为四类:

类型 含义 商业建议
Hot Spot(高-高) 高分店扎堆 可入驻,借势品牌集群效应
Cold Spot(低-低) 差评店集中 警惕,可能存在区域口碑塌方
High-Low 孤立高分店 有机会成为“破局者”
Low-High 周围都好就它差 可能存在经营问题,加强监管

想象一下,你要开一家新川菜馆,是挤进已经卷成麻花的徐家汇,还是选择尚未爆发但潜力十足的北外滩?答案不言而喻。


时间序列预测:下个月销售额能涨多少?

除了空间,时间维度也不能放过。

餐饮行业的销售曲线充满节奏感:周末高峰、节假日飙升、天气变化……传统ARIMA模型已经有点力不从心。

LSTM登场:捕捉复杂周期模式

长短期记忆网络(LSTM)擅长处理长期依赖关系。我们可以用过去30天的销量预测未来7天:

from keras.models import Sequential
from keras.layers import LSTM, Dense

def create_sequences(data, seq_length):
    xs, ys = [], []
    for i in range(len(data) - seq_length):
        x = data[i:i+seq_length]
        y = data[i+seq_length]
        xs.append(x)
        ys.append(y)
    return np.array(xs), np.array(ys)

# 归一化
data = sales_series.values.astype('float32')
scaled = (data - data.mean()) / data.std()

X, y = create_sequences(scaled, 30)
X = X.reshape(X.shape[0], X.shape[1], 1)

# 构建模型
model = Sequential([
    LSTM(50, return_sequences=True, input_shape=(30, 1)),
    LSTM(50),
    Dense(1)
])
model.compile(optimizer='adam', loss='mse')
model.fit(X, y, epochs=50, batch_size=32)

当然,LSTM也有缺点:训练慢、黑盒、难解释。所以更稳妥的做法是 融合模型 ——用ARIMA拟合趋势,LSTM预测残差。


关联规则挖掘:炸鸡为什么总配可乐?

菜单数据分析最有意思的部分来了: 菜品组合规律

使用FP-Growth算法,我们可以找出哪些菜经常一起出现:

from mlxtend.frequent_patterns import fpgrowth, association_rules

# one-hot编码订单数据
te = TransactionEncoder()
te_ary = te.fit(transactions).transform(transactions)
df_hot = pd.DataFrame(te_ary, columns=te.columns_)

# 挖掘频繁项集
frequent_itemsets = fpgrowth(df_hot, min_support=0.02, use_colnames=True)

# 生成规则
rules = association_rules(frequent_itemsets, metric="lift", min_threshold=1.2)

看看结果:

antecedents consequents lift
炸鸡 奶茶 2.60
小龙虾 冰啤酒 2.73
牛排 红酒 3.15
寿司 清酒 2.90
烤串 花生毛豆 2.93

看到没?“牛排→红酒”的提升度高达3.15,说明这不是巧合,而是强烈偏好。高端餐厅完全可以推出“主厨 pairing 推荐”,提升客单价。

甚至还可以按人群细分:

  • 高消费群体:推荐“鹅肝+勃艮第红酒”
  • 年轻上班族:打包“汉堡+薯条+可乐”套餐
  • 夜宵党:自动捆绑“烤串+冰啤+花生”

这才是真正的个性化营销。


最后的话:数据驱动的本质,是闭环思维

讲了这么多技术细节,最后想说点务虚的。

数据分析的价值,从来不在于画出了多漂亮的图表,也不在于用了多高级的模型。真正的价值,在于能否形成一个 从洞察到行动的闭环

就像这张图:

flowchart TD
    RawData[原始数据] --> Cleaning[清洗建模]
    Cleaning --> Analysis[多维挖掘]
    Analysis --> Insight[商业洞察]
    Insight --> Action[运营动作]
    Action --> Result[业务结果]
    Result --> Feedback[反馈回数据]
    Feedback --> RawData

每一次促销活动的效果,都会沉淀为新的评论和销量数据;每一次选址调整,都会改变空间分布格局。只要这个环一直转下去,企业的决策就会越来越聪明。

所以,下次当你面对一份看似平凡的数据集时,别急着跑模型。先问问自己:

这些数字背后,藏着什么样的人性、地理与时间密码?

一旦你想通了这个问题,代码自然就有了灵魂 💡

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:“上海餐饮数据.zip.zip”是一个涵盖上海市餐饮业多维度信息的综合性数据集,包含餐馆基本信息、菜品价格、用户评价、营业时间、地理坐标、消费档次、菜系分类及交易行为等数据。该数据以压缩格式存储,可能包含CSV、Excel或JSON等多种结构化文件,适用于市场研究、商业智能与学术分析。通过描述性统计、聚类分析、关联规则挖掘和时间序列预测等方法,结合Python、Tableau等工具进行可视化与建模,本项目旨在深入挖掘餐饮市场规律,洞察消费者行为,辅助企业优化运营策略,并为城市餐饮业态研究提供数据支持。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

更多推荐