Удалить выбросы из строк Pandas Pivot_table - PullRequest
1 голос
/ 10 апреля 2019

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

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

df = pd.DataFrame(
    [
    ['AABBCCDD','2014/2015','Q2',31737.60],
    ['AABBCCDD','2014/2015','Q2',31737.60],
    ['AABBCCDD','2014/2015','Q2',31737.60],
    ['AABBCCDD','2014/2015','Q3',89060.84],
    ['AABBCCDD','2015/2016','Q3',71586.00],
    ['AABBCCDD','2016/2017','Q3',89060.82],
    ['AABBCCDD','2017/2018','Q3',98564.40],
    ['AABBCCDD','2017/2018','Q3',110691.24],
    ['AABBCCDD','2017/2018','Q4',93390.00],
    ['AABBCCDD','2018/2019','Q2',90420.00],
    ['AABBCCDD','2018/2019','Q3',13.08],
    ['AABBCCDD','2018/2019','Q3',13.08]
    ], 
    columns=['PART_NO','FiscalYear','FiscalQuarter','Price'])

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

Теперь .. правильное действие - перейти к покупателю и получить его /ее исправить проблему.Я хотел бы получить обзор проблем заранее

Я попытался изменить данные

df_tab = pd.pivot_table(df, values='Price', index=['PART_NO'], columns=['FiscalYear','FiscalQuarter'], aggfunc=np.mean)

В результате следующее:

enter image description here

Естественно, у меня есть тысячи деталей, которые должны быть в этом кадре данных, с одной строкой или номером детали.Вероятно, он будет идти по дате, а не по кварталу, поэтому вышеприведенное приведено для упрощения.

Как мне поступить в следующих 2 сценариях

  1. Замена значений выброса нулевым значением, создающимстолбец в конце
  2. , указывающий, что значения выбросов присутствовали в данной строке данных.

------------- EDIT --------------

Я использовал сочетание приведенных ниже предложений и несколько других идей и пришел к следующему решению

# Imports
import pyodbc
import urllib
from sql import SQL
import pandas as pd
from sqlalchemy import create_engine

# Set variables
upperQuantile = 0.8
lowerQuantile = 0.2

# Connect to server / database
params = urllib.parse.quote_plus("Driver={SQL Server Native Client 11.0};Server=LT02670;Database=staging;Trusted_Connection=yes;")
engine = create_engine("mssql+pyodbc:///?odbc_connect=%s" % params)

# Create dataframe containing raw data
df = pd.read_sql(SQL(), engine)

# define upper and lower quartile ranges for outlier detection
def q1(x):
    return x.quantile(lowerQuantile)

def q2(x):
    return x.quantile(upperQuantile)

# define function for sorting out outliers
f = {'PO_UNIT_PRICE_CURRENT_CURRENCY': ['median', 'std', q1,q2]}

# group data and add function to data (adds columns median, std, q1 and q2)
dfgrp = df.groupby(['PART_NO']).agg(f).reset_index()

# Isolate part numbers in dataframe
dfgrpPart = pd.DataFrame(dfgrp['PART_NO'])

# Isolate value columns in dataframe
dfgrpStat = dfgrp['PO_UNIT_PRICE_CURRENT_CURRENCY']

# Join categorical data with values (this is done in order to eliminate multiindex caused py groupby function)
dfgrp = dfgrpPart.join(dfgrpStat)

# Add new columns to raw data extract
df = df.join(dfgrp.set_index('PART_NO'), on='PART_NO').reset_index()

# Remove outliers and 0-values
idx = df[df['PO_UNIT_PRICE_CURRENT_CURRENCY'] < df['q1']].index
df.drop(idx, inplace=True)
idx = df[df['PO_UNIT_PRICE_CURRENT_CURRENCY'] > df['q2']].index
df.drop(idx, inplace=True)
idx = df[df['PO_UNIT_PRICE_CURRENT_CURRENCY'] <= 0].index
df.drop(idx, inplace=True)

# Split dataframe into fiscal year chunks, and build lists of part numbers
df_14_15 = df[df['FiscalYear'].str.match('2014/2015')]['PART_NO'].to_list()
# df_15_16 = df[df['FiscalYear'].str.match('2015/2016')]['PART_NO'].to_list()
df_16_17 = df[df['FiscalYear'].str.match('2016/2017')]['PART_NO'].to_list()
# df_17_18 = df[df['FiscalYear'].str.match('2017/2018')]['PART_NO'].to_list()
df_18_19 = df[df['FiscalYear'].str.match('2018/2019')]['PART_NO'].to_list()
df_19_20 = df[df['FiscalYear'].str.match('2019/2020')]['PART_NO'].to_list()

# create one list of unique part numbers from multiple years, i have chosen only some years, as we rarely order the same parts six years running
partsList = list(set(df_14_15) & set(df_16_17) & set(df_18_19))

# Use list of part numbers to filter out raw data into output dataframe
dfAllYears = df[df['PART_NO'].isin(partsList)]

# write data to excel file for further analysis, this will overwrite existing file so be careful
dfAllYears.to_excel("output.xlsx", index=False, sheet_name='Data')

Это позволило мнепровести анализ и двигаться дальше.

Хотя я не совсем доволен кодом и считаю, что могу делать некоторые вещи излишне сложными и не использовать панд в полной мере

Ответы [ 3 ]

1 голос
/ 10 апреля 2019

Один из способов сделать это - отфильтровать столбцы с экстремальным значением (> 10%) в этом случае, но, изменяя низкий и высокий значения, вы можете установить границы экстремального значения.После этого вы можете заменить эти значения на low и high на nan, а затем взять подмножество столбцов, которые в данном случае являются выбросами, как отдельный DataFrame.

from scipy import stats
import pandas as pd
import numpy as np

df = pd.DataFrame(
    [
        ['AABBCCDD', '2014/2015', 'Q2', 31737.60],
        ['AABBCCDD', '2014/2015', 'Q2', 31737.60],
        ['AABBCCDD', '2014/2015', 'Q2', 31737.60],
        ['AABBCCDD', '2014/2015', 'Q3', 89060.84],
        ['AABBCCDD', '2015/2016', 'Q3', 71586.00],
        ['AABBCCDD', '2016/2017', 'Q3', 89060.82],
        ['AABBCCDD', '2017/2018', 'Q3', 98564.40],
        ['AABBCCDD', '2017/2018', 'Q3', 110691.24],
        ['AABBCCDD', '2017/2018', 'Q4', 93390.00],
        ['AABBCCDD', '2018/2019', 'Q2', 90420.00],
        ['AABBCCDD', '2018/2019', 'Q3', 13.08],
        ['AABBCCDD', '2018/2019', 'Q3', 13.08]
    ],
    columns=['PART_NO', 'FiscalYear', 'FiscalQuarter', 'Price'])

filt_df = df.loc[:, df.columns == 'Price']

low = .05
high = .95
quant_df = filt_df.quantile([low, high])
print(quant_df)

filt_df = filt_df.apply(lambda x: x[(x > quant_df.loc[low, x.name]) &
                                    (x < quant_df.loc[high, x.name])], axis=0)

filt_df = pd.concat([df.loc[:, 'PART_NO'], filt_df], axis=1)
filt_df = pd.concat([df.loc[:, 'FiscalYear'], filt_df], axis=1)
filt_df = pd.concat([df.loc[:, 'FiscalQuarter'], filt_df], axis=1)

Outliers = filt_df[filt_df.isnull().any(axis=1)]

print(Outliers)

Вывод:

   FiscalQuarter FiscalYear   PART_NO  Price
7             Q3  2017/2018  AABBCCDD    NaN
10            Q3  2018/2019  AABBCCDD    NaN
11            Q3  2018/2019  AABBCCDD    NaN

В этом случае я не уверен, что индекс 7 является правильным или неправильным.Но вы можете указать границы, которые вы хотите, при условии, что они находятся между 0 и 1. Затем с отфильтрованным DataFrame посмотрите, какие из них выделяются больше всего.

1 голос
/ 10 апреля 2019

Я думаю, что сравнение каждой цены со средней для этого PART_NO будет легко показать (при условии, что цены не колеблются часто).

import pandas as pd

df = pd.DataFrame(
[
    ['AABBCCDD', '2014/2015', 'Q2', 31737.60],
    ['AABBCCDD', '2014/2015', 'Q2', 31737.60],
    ['AABBCCDD', '2014/2015', 'Q2', 31737.60],
    ['AABBCCDD', '2014/2015', 'Q3', 89060.84],
    ['AABBCCDD', '2015/2016', 'Q3', 71586.00],
    ['AABBCCDD', '2016/2017', 'Q3', 89060.82],
    ['AABBCCDD', '2017/2018', 'Q3', 98564.40],
    ['AABBCCDD', '2017/2018', 'Q3', 110691.24],
    ['AABBCCDD', '2017/2018', 'Q4', 93390.00],
    ['AABBCCDD', '2018/2019', 'Q2', 90420.00],
    ['AABBCCDD', '2018/2019', 'Q3', 13.08],
    ['AABBCCDD', '2018/2019', 'Q3', 13.08]
],
columns=['PART_NO', 'FiscalYear', 'FiscalQuarter', 'Price'])


avg_df = df.groupby('PART_NO').mean(['Price'].to_frame().reset_index().rename(columns={'Price': 'AVG_PRICE'})

df = df.merge(avg_df)

df['ratio'] = df['AVG_PRICE']/df['Price']

Выход:

     PART_NO FiscalYear FiscalQuarter      Price     AVG_PRICE        ratio
0   AABBCCDD  2014/2015            Q2   31737.60  61501.021667     1.937797
1   AABBCCDD  2014/2015            Q2   31737.60  61501.021667     1.937797
2   AABBCCDD  2014/2015            Q2   31737.60  61501.021667     1.937797
3   AABBCCDD  2014/2015            Q3   89060.84  61501.021667     0.690551
4   AABBCCDD  2015/2016            Q3   71586.00  61501.021667     0.859121
5   AABBCCDD  2016/2017            Q3   89060.82  61501.021667     0.690551
6   AABBCCDD  2017/2018            Q3   98564.40  61501.021667     0.623968
7   AABBCCDD  2017/2018            Q3  110691.24  61501.021667     0.555609
8   AABBCCDD  2017/2018            Q4   93390.00  61501.021667     0.658540
9   AABBCCDD  2018/2019            Q2   90420.00  61501.021667     0.680171
10  AABBCCDD  2018/2019            Q3      13.08  61501.021667  4701.912971
11  AABBCCDD  2018/2019            Q3      13.08  61501.021667  4701.912971

Соотношение огромно для выброса. Если вы выберете df.ratio > 5 или любой другой номер, который выберете, он получит все нужные вам записи.

1 голос
/ 10 апреля 2019

Чтобы правильно определить, является ли что-то посторонним, вам нужно добавить статистику в микс.Это немного выходит за рамки того, что вам нужно сделать.

Я бы рекомендовал просто отсортировать по убыванию и посмотреть на верхние значения в кадре данных.

Вы можете сделать это:

df = df.sort_values('Price').reset_index()

Чтобы заменить эти значения на ноль, вы можете просто просмотреть индекс и выбрать все значения Price в диапазоне и установить их на None.

...