March 26, 2021

Фантомные ошибки обработки файлов с Python

Следует запомнить эти рекомендации, чтобы избежать сюрпризов с обработкой файлов в будущем. Сам я о них вовремя не узнал и потерял много времени на выявление постоянно возникавших ошибок.

Изучение вопроса проведем на примере сгенерированного игрушечного набора data (код представлен в конце статьи) о заработке людей из разных городов (идентифицируются по индексу) следующего вида:

Некорректное распознавание строк

Поле "индекс_города" состоит из семи случайных цифр от 0 до 9, которые в некоторых случаях разделяются символами "/" или "-". После сохранения data в файл и загрузки обратно в df не все значения этого поля корректно распознаются. Например, при выдаче длин строк в ячейках "индекс_города" (всего 100) получаем следующее:

df['индекс_города'].str.len().value_counts()

Найдем значения, длины которых не распознаны:

df.loc[~df['индекс_города'].str.len().isin([7,8]),'индекс_города'] 

Похоже не распознаны строки с индексами, которые выглядят как числа (то есть не разделены "/" или "-"). Для борьбы с эти следует явно привести столбец к строчному типу:

df['индекс_города'] = df['индекс_города'].astype(str)

Теперь повторно получим длины строк в ячейках "индекс_города":

df['индекс_города'].str.len().value_counts()

Строки из цифр, начинающихся с нуля

То же поле "индекс_города" после сохранения сгенерированных данных в файл и загрузки в таблицу df предоставляет очередной сюрприз. В частности, появляется индекс города, содержащий только 6 символов, что видно из предыдущей картинки.

df[df['индекс_города'].str.len()==6]

Оказывается, в первоначальной таблице содержался индекс 0319966, который после сохранения в Excel таблицу и загрузки обратно потерял первый символ. Вероятно, это обусловлено тем фактом, что текстовый процессор воспринимает последовательность цифр как число и не видит смысла в начальном 0. Для справки, если вы попытаетесь в Microsoft Excel или Google Таблицах сохранить в ячейке аналогичную последовательность символов, то 0 будет потерян.

Проблема решается путем чтения файлов функциями Pandas (read_excel или read_csv) с параметром dtype ='str'. Кстати, этим же способом можно решить и первую проблему.

Числа с плавающей точкой

Поле "зарплата" также заставляет нас помучиться. В частности, процедура выгрузки-загрузки данных приводит к изменениям в количестве сохраняемых символов после запятой. Однако установить это не так просто. Попытаемся вывести все значения зарплаты, которые не совпадают у одних и тех же людей:

fio_z1 = df.loc[(~df['зарплата'].
      isin(data['зарплата'])) & (df['ФИО'].isin(data['ФИО'])),
                ['ФИО','зарплата']]
fio_z2 = data.loc[(~data['зарплата'].
      isin(df['зарплата'])) & (data['ФИО'].isin(df['ФИО'])),
                  ['ФИО','зарплата']]
df_m = pd.merge(fio_z1,fio_z2,on='ФИО')

Различия не визуализируются, кажется, что сохранение происходит до 4 символа. Однако, если вывести эти же данные в командную строку, получим другой результат:

Если же просмотреть файл с данными, то получим "третью" версию точности сохранения:

Мораль из этого проста - перед сохранением округляем данные до требуемой точности:

data['зарплата'] = np.round(data['зарплата'],2)

Теперь расхождений не будет:

fio_z1 = df.loc[(~df['зарплата'].
      isin(data['зарплата'])) & (df['ФИО'].isin(data['ФИО'])),
                ['ФИО','зарплата']]
fio_z2 = data.loc[(~data['зарплата'].
      isin(df['зарплата'])) & (data['ФИО'].isin(df['ФИО'])),
                  ['ФИО','зарплата']]
df_m = pd.merge(fio_z1,fio_z2,on='ФИО')

Таким образом, можно сделать следующие выводы:

  1. в определенных случаях читаем файлы с параметром dtype ='str' и явно приводим поля к соответствующим им типам;
  2. перед сохранением значений с плавающей точкой, округляем их.

Ниже привожу код для генерации данных:

import pandas as pd
import numpy as np
import string


np.random.seed(0)

letters = np.array(list(string.ascii_uppercase))
digits = np.arange(0,10)
people_num = 100
towns_num = 20 # количество городов, нацело делит people_num
file_size = 10 # на столько файлов поделим данные (нацело делит people_num)

# людям соответствуют по две случайные буквы алфавита (имя, фамилия)
names = np.random.choice(letters, size=(people_num,2))
# зарплата от 20 до 50 тыс. рублей, случайное равномерное распределение
salary = np.random.uniform(20,50, size=(people_num))
# индекс - по семь цифр из алфавита, их меньше чем людей,
# так как предполагаем одинаковые города для разных людей
loc_ind_p = np.random.choice(digits, size=(towns_num,7))
loc_ind = np.tile(loc_ind_p, (people_num//towns_num,1))

# склеим буквы имен и фамилий, а также
# индексы городов
names = pd.DataFrame(names).apply(lambda x:(x[0]+' '+x[1]), axis=1)
loc_ind = pd.DataFrame(loc_ind).astype(str).apply(lambda x:''.join(x),axis=1)

# две трети индексов для правдоподобия будут записаны другими способами
part = loc_ind.shape[0]//3
loc_ind.iloc[:part] = loc_ind.iloc[:part]\
.map(lambda x: x[:3]+'-'+x[3:])
loc_ind.iloc[part:part*2] = loc_ind.iloc[part:part*2]\
.map(lambda x: x[:3]+'/'+x[3:])


data = pd.DataFrame({'ФИО':names, 'индекс_города':loc_ind,
'зарплата':salary})

data['зарплата'] = np.round(data['зарплата'],2)

# выгружаем на диск
for i in np.arange(data.shape[0]//file_size):
data[i*file_size:(i+1)*file_size].to_excel(f'data/files/peop_{i}_.xlsx',
index=False)