脚本专家 发表于 4 天前

Python问卷数据清洗实战:直线作答秒填规律异常检测代

在问卷研究中,回收的数据难免混入大量无效样本:直线作答、秒填、规律选择、前后矛盾等。这些数据若不清理,会严重污染分析结果。本文提供一套完整的Python清洗流程,覆盖最常见的无效样本类型,并封装为可复用的类。

## 环境准备
需要 Python 3.8+,以及 pandas、numpy、matplotlib。安装命令:

pip install pandas numpy matplotlib openpyxl

问卷星导出的数据通常结构为:一行一份问卷,一列一个题目,包含序号、提交时间、各题目答案。

## 数据加载与列识别
使用 pandas 读取 Excel 或 CSV 文件:

import pandas as pd
import numpy as np
df = pd.read_excel('问卷数据.xlsx')
print(f"原始数据量: {len(df)} 份")

自动识别题目列(包含 'Q'、'第'、'题' 等关键词)和时间列:

question_cols =
time_col =
print(f"题目列: {question_cols}")
print(f"时间列: {time_col}")


## 检测直线作答(Straight-lining)
所有题目选择完全相同的答案,是最常见的无效模式。以下函数检测完全一致的样本:

def detect_straight_lining(df, question_cols):
    invalid_indices = []
    for idx, row in df.iterrows():
      answers = row.dropna().tolist()
      if len(answers) < 3:
            continue
      if len(set(answers)) == 1:
            invalid_indices.append(idx)
    return set(invalid_indices)

为了避免误判(例如全部选“满意”可能是真实),增加接近直线检测:超过80%的答案相同。

from collections import Counter
def detect_near_straight_lining(df, question_cols, threshold=0.8):
    invalid_indices = []
    for idx, row in df.iterrows():
      answers = row.dropna().tolist()
      if len(answers) < 3:
            continue
      counts = Counter(answers)
      most_common_ratio = counts.most_common(1) / len(answers)
      if most_common_ratio >= threshold:
            invalid_indices.append(idx)
    return set(invalid_indices)


## 检测秒填(Speeders)
如果问卷包含答题时长列,直接按阈值过滤。常见规则:10题以内<10秒,10-20题<20秒,20题以上<30秒。

def detect_speeders(df, time_col, min_seconds=15):
    if not time_col:
      return set()
    duration_col =
    if duration_col:
      durations = pd.to_numeric(df], errors='coerce')
      return set(df.index)
    return set()


## 检测规律作答(Pattern Response)
如 1-2-3-4-5 的递增/递减序列,或 A-B-A-B 的交替模式。

def detect_pattern_response(df, question_cols):
    invalid_indices = []
    for idx, row in df.iterrows():
      answers = row.dropna().tolist()
      if len(answers) < 5:
            continue
      try:
            numeric = pd.to_numeric(answers)
      except:
            if len(answers) >= 6:
                half = len(answers) // 2
                if answers[:half] == answers:
                  invalid_indices.append(idx)
            continue
      diffs = np.diff(numeric)
      if np.all(diffs == diffs) and diffs != 0:
            invalid_indices.append(idx)
            continue
      if len(numeric) >= 6:
            even_vals = set(numeric[::2])
            odd_vals = set(numeric)
            if len(even_vals) == 1 and len(odd_vals) == 1:
                invalid_indices.append(idx)
    return set(invalid_indices)


## 检测矛盾回答
针对跳题逻辑,例如 Q1选“否”但后续题仍有内容。需要根据具体问卷编写:

def detect_contradictions(df, filter_col, filter_value, check_col):
    mask = df == filter_value
    should_be_empty = df.loc
    contradictions = should_be_empty
    return set(contradictions.index)


## 检测异常值(Z-score)
对量表题(1-5分或1-7分)计算每个样本的均值Z分数,超过阈值(通常3.0)视为异常:

def detect_outliers_zscore(df, question_cols, threshold=3.0):
    numeric_df = df.apply(pd.to_numeric, errors='coerce')
    row_means = numeric_df.mean(axis=1)
    z_scores = (row_means - row_means.mean()) / row_means.std()
    return set(df.index)


## 汇总无效样本与标记
合并所有检测结果,并在DataFrame中添加标记列,导出清洗后的有效数据和无效明细:

all_invalid = straight_lining | near_straight | speeders | patterns | outliers
df['is_invalid'] = df.index.isin(all_invalid)
df['invalid_reason'] = ''
df.loc += '直线作答;'
df.loc += '接近直线;'
df.loc += '秒填;'
df.loc += '规律作答;'
df.loc += '统计异常;'
clean_df = df[~df['is_invalid']].copy()
clean_df.to_excel('问卷数据_已清洗.xlsx', index=False)
invalid_df = df].copy()
invalid_df.to_excel('无效样本明细.xlsx', index=False)


## 可视化清洗结果
使用matplotlib绘制无效类型柱状图和有效率饼图(需安装中文字体):

import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei']
plt.rcParams['axes.unicode_minus'] = False
reasons = {
    '直线作答': len(straight_lining),
    '接近直线': len(near_straight - straight_lining),
    '秒填': len(speeders),
    '规律作答': len(patterns),
    '统计异常': len(outliers),
}
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
ax1.bar(reasons.keys(), reasons.values(), color='#ff6b6b')
ax1.set_title('无效样本类型分布')
ax2.pie(, labels=['有效样本', '无效样本'], autopct='%1.1f%%', colors=['#51cf66', '#ff6b6b'])
ax2.set_title(f'样本有效率 (总计 {len(df)} 份)')
plt.tight_layout()
plt.show()


## 封装为可复用的类
将上述函数整合成 SurveyDataCleaner 类,支持一键运行:

import pandas as pd
import numpy as np
from collections import Counter

class SurveyDataCleaner:
    def __init__(self, filepath: str, question_cols: list = None):
      if filepath.endswith('.xlsx'):
            self.df = pd.read_excel(filepath)
      else:
            self.df = pd.read_csv(filepath)
      self.question_cols = question_cols or self._auto_detect_questions()
      self.invalid_sets = {}

    def _auto_detect_questions(self):
      return )]

    def run_all(self, min_seconds=15, z_threshold=3.0, straight_threshold=0.8):
      self.invalid_sets['直线作答'] = self._detect_straight()
      self.invalid_sets['接近直线'] = self._detect_near_straight(straight_threshold)
      self.invalid_sets['秒填'] = self._detect_speeders(min_seconds)
      self.invalid_sets['规律作答'] = self._detect_pattern()
      self.invalid_sets['统计异常'] = self._detect_outliers(z_threshold)
      return self

    def _detect_straight(self):
      invalid = set()
      for idx, row in self.df.iterrows():
            ans = row.dropna().tolist()
            if len(ans) >= 3 and len(set(ans)) == 1:
                invalid.add(idx)
      return invalid

    def _detect_near_straight(self, threshold):
      invalid = set()
      for idx, row in self.df.iterrows():
            ans = row.dropna().tolist()
            if len(ans) >= 3:
                counts = Counter(ans)
                if counts.most_common(1) / len(ans) >= threshold:
                  invalid.add(idx)
      return invalid

    def _detect_speeders(self, min_seconds):
      duration_col =
      if not duration_col:
            return set()
      durations = pd.to_numeric(self.df], errors='coerce')
      return set(self.df.index)

    def _detect_pattern(self):
      invalid = set()
      for idx, row in self.df.iterrows():
            ans = row.dropna().tolist()
            if len(ans) < 5:
                continue
            try:
                numeric = pd.array(ans, dtype='float')
                diffs = np.diff(numeric)
                if len(diffs) > 0 and np.all(diffs == diffs) and diffs != 0:
                  invalid.add(idx)
            except:
                pass
      return invalid

    def _detect_outliers(self, threshold):
      numeric_df = self.df.apply(pd.to_numeric, errors='coerce')
      row_means = numeric_df.mean(axis=1)
      z_scores = (row_means - row_means.mean()) / row_means.std()
      return set(self.df.index)

    @property
    def all_invalid(self):
      result = set()
      for s in self.invalid_sets.values():
            result |= s
      return result

    @property
    def clean_data(self):
      return self.df[~self.df.index.isin(self.all_invalid)].copy()

    def report(self):
      total = len(self.df)
      invalid_count = len(self.all_invalid)
      print(f"\n{'='*50}")
      print(f"问卷数据清洗报告")
      print(f"{'='*50}")
      print(f"原始样本量: {total} 份")
      for reason, indices in self.invalid_sets.items():
            print(f"{reason}: {len(indices)} 份")
      print(f"{'─'*50}")
      print(f"无效样本合计: {invalid_count} 份")
      print(f"有效样本: {total - invalid_count} 份")
      print(f"有效率: {(1 - invalid_count/total)*100:.1f}%")
      print(f"{'='*50}")

    def export(self, output_path='清洗结果'):
      clean = self.clean_data
      clean.to_excel(f'{output_path}_有效数据.xlsx', index=False)
      invalid_df = self.df.copy()
      invalid_df['无效原因'] = ''
      for reason, indices in self.invalid_sets.items():
            invalid_df.loc += f'{reason};'
      invalid_df.to_excel(f'{output_path}_无效样本.xlsx', index=False)

if __name__ == '__main__':
    cleaner = SurveyDataCleaner('问卷数据.xlsx')
    cleaner.run_all(min_seconds=15, z_threshold=3.0)
    cleaner.report()
    cleaner.export()


## 关于样本质量的延伸
清洗后有效样本量可能不足。若无效率超过20%,建议检查数据来源。例如,相对于互填群,使用真实用户渠道(如问卷鸭等)的无效率通常更低。但无论来源如何,清洗流程不可或缺。以上代码覆盖了直线作答、秒填、规律模式、矛盾回答和统计异常等常见场景,可用于学术调研、市场研究等。如需进一步信效度分析,可结合 factor_analyzer 库。

热心网友5 发表于 4 天前

Re: Python问卷数据清洗实战:直线作答秒填规律异常检测代

感谢分享,这套清洗流程很实用,尤其是近直线检测和交替模式判断,考虑到了真实填写中可能出现的模糊情况。想请教一下,对于混合型问卷(既有评分题又有选择题),你是如何区分哪些列适用于这些检测逻辑的?另外,如果数据量很大(比如上万份),直接用循环逐行遍历会不会影响效率,有没有考虑用向量化操作来优化?

热心网友5 发表于 4 天前

Re: Python问卷数据清洗实战:直线作答秒填规律异常检测代

非常实用的分享!最近正好在处理一批问卷数据,直线作答和秒填确实是最头疼的问题。你提供的检测函数思路清晰,尤其是“接近直线检测”用占比阈值来避免误判,很合理。 想请教一下:在实际应用中,不同题目类型的取值范围差异较大(比如李克特5点量表 vs 多选题),你会建议针对不同的题目块分别设定阈值吗?另外,如果问卷中包含矩阵题(比如一行多题),题目列识别时是否需要特殊处理?期待继续补充逻辑矛盾检测的部分~

热心网友5 发表于 4 天前

Re: Python问卷数据清洗实战:直线作答秒填规律异常检测代

感谢分享!代码框架很清晰,直接复制就能跑起来,对处理问卷数据非常实用。我之前也遇到过直线作答和规律填答的问题,手动筛选太费时了,你这个封装成类的方式很棒。想请教一下,在实际项目中,如果问卷包含多选题(列名不规则),是否需要对question_cols的识别逻辑做额外处理?另外对于秒填检测,如果数据里没有单独的时长列,只靠提交时间计算间隔可行吗?希望多交流。
页: [1]
查看完整版本: Python问卷数据清洗实战:直线作答秒填规律异常检测代