¿Por qué usar pandas qcut return ValueError: los bordes del contenedor deben ser únicos?

Tengo conjunto de datos

recency;frequency;monetary 21;156;41879955 13;88;16850284 8;74;79150488 2;74;26733719 9;55;16162365 ...;...;... 

detalle los datos en bruto -> http://pastebin.com/beiEeS80 y puse en DataFrame y aquí está mi código completo:

 df = pd.DataFrame(datas, columns=['userid', 'recency', 'frequency', 'monetary']) df['recency'] = df['recency'].astype(float) df['frequency'] = df['frequency'].astype(float) df['monetary'] = df['monetary'].astype(float) df['recency'] = pd.qcut(df['recency'].values, 5).codes + 1 df['frequency'] = pd.qcut(df['frequency'].values, 5).codes + 1 df['monetary'] = pd.qcut(df['monetary'].values, 5).codes + 1 

pero es un error de retorno

 df['frequency'] = pd.qcut(df['frequency'].values, 5).codes + 1 ValueError: Bin edges must be unique: array([ 1., 1., 2., 4., 9., 156.]) 

¿Cómo resolver esto?

Ejecuté esto en Jupyter y coloqué exampledata.txt en el mismo directorio que el cuaderno.

Tenga en cuenta que la primera línea:

 df = pd.DataFrame(datas, columns=['userid', 'recency', 'frequency', 'monetary']) 

carga la columna 'userid' cuando no está definida en el archivo de datos. Quité este nombre de columna.

Solución

 import pandas as pd def pct_rank_qcut(series, n): edges = pd.Series([float(i) / n for i in range(n + 1)]) f = lambda x: (edges >= x).argmax() return series.rank(pct=1).apply(f) datas = pd.read_csv('./exampledata.txt', delimiter=';') df = pd.DataFrame(datas, columns=['recency', 'frequency', 'monetary']) df['recency'] = df['recency'].astype(float) df['frequency'] = df['frequency'].astype(float) df['monetary'] = df['monetary'].astype(float) df['recency'] = pct_rank_qcut(df.recency, 5) df['frequency'] = pct_rank_qcut(df.frequency, 5) df['monetary'] = pct_rank_qcut(df.monetary, 5) 

Explicación

El problema que estaba viendo era el resultado de pd.qcut suponiendo que 5 bandejas de igual tamaño. En los datos que proporcionó, la 'frequency' tiene más del 28% de número 1. Esto rompió qcut .

Proporcioné una nueva función pct_rank_qcut que aborda esto y pct_rank_qcut todos los 1 en la primera bandeja.

  edges = pd.Series([float(i) / n for i in range(n + 1)]) 

Esta línea define una serie de aristas de percentiles en función del número deseado de intervalos definidos por n . En el caso de n = 5 los bordes serán [0.0, 0.2, 0.4, 0.6, 0.8, 1.0]

  f = lambda x: (edges >= x).argmax() 

esta línea define una función auxiliar que se aplicará a otra serie en la siguiente línea. edges >= x devolverá una serie igual en longitud a los edges donde cada elemento es True o False dependiendo de si x es menor o igual que ese borde. En el caso de x = 0.14 el resultado (edges >= x) será [False, True, True, True, True, True] . Al tomar el argmax() , he identificado el primer índice donde la serie es True , en este caso 1 .

  return series.rank(pct=1).apply(f) 

Esta línea toma la series entrada y la convierte en una clasificación percentil. Puedo comparar estas clasificaciones con los bordes que he creado y es por eso que uso la apply(f) . Lo que se devuelve debe ser una serie de números de bin numerados de 1 a n. Esta serie de números de contenedores es lo mismo que intentabas obtener con:

 pd.qcut(df['recency'].values, 5).codes + 1 

Esto tiene consecuencias, ya que las bandejas ya no son iguales y la banca 1 toma prestado completamente de la bandeja 2. Pero hubo que tomar una decisión. Si no te gusta esta opción, usa el concepto para construir tu propio ranking.

Demostración

 print df.head() recency frequency monetary 0 3 5 5 1 2 5 5 2 2 5 5 3 1 5 5 4 2 5 5 

Actualizar

pd.Series.argmax() ahora está en desuso. ¡Simplemente cambie a pd.Series.values.argmax()() para actualizar!

 def pct_rank_qcut(series, n): edges = pd.Series([float(i) / n for i in range(n + 1)]) f = lambda x: (edges >= x).values.argmax() return series.rank(pct=1).apply(f) 

Varias soluciones se discuten aquí , pero brevemente:

si está utilizando pandas,> = 0.20.0 agregaron una opción duplicados = ‘boost’ | ‘soltar’ para controlar si se debe boost en bordes duplicados o soltarlos, lo que daría como resultado menos contenedores de los especificados, y algunos más grandes ( con más elementos) que otros.

Para versiones anteriores de pandas, intente pasar los valores clasificados en lugar de los valores en sí mismos:

 pd.qcut(df['frequency'].rank(method='first').values, 5).codes + 1 

De esta manera, puede hacer que valores idénticos entren en cuantiles diferentes. Esto podría ser correcto o no dependiendo de sus necesidades específicas (si esto no es lo que quiere, probablemente quiera echar un vistazo a pandas.cut que elige que las bandejas estén espaciadas de manera uniforme de acuerdo con los valores mismos, mientras que pandas.qcut elige la opción contenedores para que tenga el mismo número de registros en cada contenedor)