Фантомные ошибки обработки файлов с 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='ФИО')
Таким образом, можно сделать следующие выводы:
- в определенных случаях читаем файлы с параметром dtype ='str' и явно приводим поля к соответствующим им типам;
- перед сохранением значений с плавающей точкой, округляем их.
Ниже привожу код для генерации данных:
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)