pandas-js
Version:
Pandas for JavaScript
1,371 lines (1,256 loc) • 40.3 kB
JavaScript
/**
* @flow
* A pandas.Series one-dimensional array with axis labels, with an Immutable.List instead of
* numpy.ndarray as the values
*/
import Immutable from 'immutable';
import NDFrame from './generic';
import {enumerate, sum, parseIndex, round10,
OP_CUMSUM, OP_CUMMUL, OP_CUMMAX, OP_CUMMIN, generateCumulativeFunc} from './utils';
import {DType, arrayToDType} from './dtype';
declare type T_MF = (value: number, idx: number) => number | string;
declare type T_BF = (value1: number, value2: number) => boolean;
export default class Series extends NDFrame {
/**
* One dimensional array with axis labels. An `Immutable.List` serves as the numpy.ndarray for
* values.
*
* Operations between `Series` (+, -, /, *, **) align values based on their associated index
* values
*
* @param {Array|List} data
* Data to be stored in Series
* @param {Object} kwargs
* Extra optional arguments for a Series
* @param {string} [kwargs.name='']
* The _name to assign to the Series
* @param {Array|List} [kwargs.index]
*
* @example
* const ds = new Series([1, 2, 3, 4], {name: 'My test name', index: [2, 3, 4, 5]})
* ds.toString()
* // Returns:
* // 2 1
* // 3 2
* // 4 3
* // 5 4
* // Name: My test name, dtype: dtype(int)
*/
constructor(data: Array<Object> | Object, kwargs: Object = {}) {
super(data, kwargs);
if (Array.isArray(data)) {
this._values = Immutable.List(data);
this._dtype = arrayToDType(data);
} else if (data instanceof Immutable.List) {
this._values = data;
this._dtype = arrayToDType(data);
} else {
this._values = Immutable.List.of(data);
this._dtype = arrayToDType([data]);
}
this._name = typeof kwargs.name !== 'undefined' ? kwargs.name : '';
this.set_axis(0, parseIndex(kwargs.index, this.values));
this._setup_axes(Immutable.List.of(0));
// $FlowFixMe TODO
this._sort_ascending = this._sort_ascending.bind(this);
// $FlowFixMe TODO
this._sort_descending = this._sort_descending.bind(this);
}
// $FlowFixMe
[Symbol.iterator]() {
const values = this.values;
let index = -1;
return {
next: () => {
index += 1;
return {value: values.get(index), done: !(index >= 0 && index < values.size)};
},
};
}
/**
* Return a new `Series` created by a map along a `Series`
*
* pandas equivalent: [Series.map](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.map.html)
*
* @param {function} func
* Function to apply along the values
*
* @returns {Series}
*
* @example
* const ds = new Series([1, 2, 3, 4], {name: 'New Series'})
*
* // Returns Series([1, 4, 9, 16], {name: 'New Series', index: [1, 2]})
* ds.map((val, idx) => val ** 2);
*/
map(func: T_MF): Series {
return new Series(
this.values.map((val, idx) => func(val, idx)),
{name: this.name, index: this.index});
}
/**
* forEach applied along the `Series` values
*
* @param {function} func
* Function to apply along the values
*
* @returns {Series}
*
* @example
* const ds = new Series([1, 2, 3, 4], {name: 'New Series'})
*
* // Logs 1, 4, 9, 16
* ds.forEach((val, idx) => console.log(val ** 2));
*/
forEach(func: T_MF): void {
this.values.forEach((val, idx) => func(val, idx));
}
/**
* Return the `Series` as a string
*
* @returns {string}
*
* @example
* const ds = new Series([1, 2, 3, 4], {name: 'My test name', index: [2, 3, 4, 5]})
* ds.toString()
* // Returns:
* // 2 1
* // 3 2
* // 4 3
* // 5 4
* // Name: My test name, dtype: dtype(int)
*/
toString(): string {
// $FlowFixMe TODO
const vals = this.iloc(0, 10).values;
let valString = '';
vals.forEach((v, idx) => {
valString += `${this.index.get(idx)}\t${v}\n`;
});
return `${valString}Name: ${this.name}, dtype: ${this.dtype}`;
}
/**
* Return first n rows
*
* pandas equivalent: [Series.head](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.head.html)
*
* @param {number} n=5
* @returns {Series}
*
* @example
* const ds = new Series([1, 2, 3, 4, 5, 6, 7, 8]);
*
* // Returns Series([1, 2, 3, 4, 5])
* ds.head();
*
* // Returns Series([1, 2, 3])
* ds.head(3);
*/
head(n: number = 5): Series {
// $FlowFixMe TODO
return this.iloc(0, n);
}
/**
* Return last n rows
*
* pandas equivalent: [Series.tail](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.tail.html)
*
* @param {number} n=5
* @returns {Series}
*
* @example
* const ds = new Series([1, 2, 3, 4, 5, 6, 7, 8]);
*
* // Returns Series([4, 5, 6, 7, 8])
* ds.tail();
*
* // Returns Series([6, 7, 8])
* ds.tail(3);
*/
tail(n: number = 5): Series {
// $FlowFixMe TODO
return this.iloc(this.length - n, this.length);
}
/**
* Return a new deep copy of the `Series`
*
* pandas equivalent: [Series.copy](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.copy.html)
*
* @returns {Series}
*
* @example
* const ds = new Series([1, 2, 3], {name: 'Test 1'});
* const ds2 = ds.copy();
* ds2.index = [1, 2, 3];
* ds.index // [0, 1, 2];
* ds2.index // [1, 2, 3];
*/
copy(): Series {
return new Series(this.values, {index: this.index, name: this.name});
}
get kwargs(): Object {
return {
name: this.name,
index: this.index,
};
}
/**
* Return the dtype of the underlying data
*
* pandas equivalent [Series.dtype](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.dtype.html)
*
* @returns {DType}
*
* @example
* const ds = new Series([1.5, 2, 3], {name: 'Series name'});
* ds.dtype; // dtype('float');
*/
get dtype(): DType {
return this._dtype;
}
/**
* Return the index of the `Series`, an `Immutable.List`
*
* @returns {List}
*
* @example
* const ds = new Series([1.5, 2, 3], {name: 'Series name'});
*
* // Returns List [0, 1, 2]
* ds.index;
*/
get index(): Immutable.List {
return this._get_axis(0);
}
/**
* Set the index of the `Series`, an `Immutable.List`
*
* @param {List|Array} index
* The next values for the index of the `Series`
*
* @example
* const ds = new Series([1.5, 2, 3], {name: 'Series name'});
* ds.index = [1, 2, 3];
*
* // Returns List [1, 2, 3]
* ds.index;
*/
set index(index: Immutable.List | Array<number | string>) {
this.set_axis(0, parseIndex(index, this.values));
}
/**
* Return the length of the `Series`
*
* pandas equivalent: len(series);
*
* @returns {number}
*
* @example
* const ds = new Series([1.5, 2, 3], {name: 'Series name'});
*
* // Returns 3
* ds.length;
*/
get length(): number {
return this.values.size;
}
/**
* Return the values of the `Series` as an `Immutable.List`
*
* pandas equivalent: [Series.values](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.values.html);
*
* @returns {List}
*
* @example
* const ds = new Series([1.5, 2, 3], {name: 'Series name'});
*
* // Returns List [1.5, 2, 3]
* ds.values;
*/
get values(): Immutable.List {
return super.values;
}
/**
* Return the name of the `Series`
*
* @returns {string}
*/
get name(): string {
return this._name;
}
/**
* Convert the series to the desired type
*
* pandas equivalent: [Series.astype](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.astype.html)
*
* @param {DType} nextType
*
* @example
* const ds = new Series([1.5, 2, 3], {name: 'Series name'});
*
* // dtype('float')
* ds.dtype;
*
* // Series([1, 2, 3])
* ds.astype(new DType('int'))
*/
astype(nextType: DType): Series {
if (!(nextType instanceof DType))
throw new Error('Next type must be a DType');
if (nextType.dtype === this.dtype)
return this;
switch (nextType.dtype) {
case 'int': {
if (this.dtype.dtype === 'object') throw new Error('Cannot convert object to int');
const kwargs = {name: this.name, index: this.index};
return new Series(this.values.map(v => Math.floor(v)), kwargs);
}
case 'float': {
if (this.dtype.dtype === 'object') throw new Error('Cannot convert object to float');
const kwargs = {name: this.name, index: this.index};
return new Series(this.values.map(v => parseFloat(v)), kwargs);
}
default:
throw new Error(`Invalid dtype ${nextType}`);
}
}
/**
* Return the Series between [startVal, endVal), or at startVal if endVal is undefined
*
* pandas equivalent: [Series.iloc](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.iloc.html)
*
* @param {int} startVal
* @param {int} [endVal]
*
* @returns {Series|number|string}
*
* @example
* const ds = new Series([1, 2, 3, 4], {name: 'New Series'})
* ds.iloc(1) // 2
* ds.iloc(1, 3) // Series([2, 3], {name: 'New Series', index: [1, 2]})
*/
iloc(startVal: number, endVal: ?number): Series | number | string {
if (typeof endVal === 'undefined')
return this.values.get(startVal);
const {name} = this.kwargs;
const index = this.index.slice(startVal, endVal);
return new Series(this.values.slice(startVal, endVal), {name, index});
}
/**
* Return the sum of the values in the `Series`
*
* pandas equivalent: [Series.sum](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.sum.html)
*
* @returns {number}
*
* @example
* const ds = new Series([1, 2, 3, 4], {name: 'New Series'})
*
* // Returns 10
* ds.sum();
*/
sum(): number {
return sum(this.values);
}
/**
* Return the mean of the values in the `Series`
*
* pandas equivalent: [Series.mean](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.mean.html)
*
* @returns {number}
*
* @example
* const ds = new Series([1, 2, 3, 4], {name: 'New Series'})
*
* // Returns 2.5
* ds.mean();
*/
mean(): number {
return this.sum() / this.length;
}
/**
* Return the median of the values in the `Series`
*
* pandas equivalent: [Series.median](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.median.html)
*
* @returns {number}
*
* @example
* const ds = new Series([2, 3, 1, 4, 5], {name: 'New Series'})
*
* // Returns 3
* ds.median();
*/
median(): number {
const sortedVals = this.values.sort();
if (this.length % 2 === 1)
return sortedVals.get(Math.floor(this.length / 2));
const halfLength = this.length / 2;
return (sortedVals.get(halfLength - 1) + sortedVals.get(halfLength)) / 2;
}
/**
* Return the variance of the values in the `Series`
*
* pandas equivalent: [Series.var](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.var.html)
*
* @returns {number}
*
* @example
* const ds = new Series([1, 2, 3], {name: 'New Series'})
*
* // Returns 1
* ds.variance();
*/
variance(): number {
const mean = this.mean();
return this.values.reduce((s, v) => {
const diff = v - mean;
return s + ((diff * diff) / (this.length - 1));
}, 0);
}
/**
* Return the standard deviation of the values in the `Series`
*
* pandas equivalent: [Series.std](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.std.html)
*
* @returns {number}
*
* @example
* const ds = new Series([1, 2, 3], {name: 'New Series'})
*
* // Returns 1
* ds.std();
*/
std(): number {
return Math.sqrt(this.variance());
}
/**
* Return Series with absolute value of all values
*
* pandas equivalent: [Series.abs](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.abs.html)
*
* @returns {Series}
*
* @example
* const ds = new Series([-1, 2, -4, 5, -1, -2]);
*
* // Returns Series([1, 2, 4, 5, 1, 2]);
* ds.abs();
*/
abs(): Series {
if (['bool', 'string', 'object'].indexOf(this.dtype.dtype) >= 0) return this.copy();
return new Series(this.values.map(v => Math.abs(v)), {name: this.name, index: this.index});
}
_combineOp(other: any, op: (a: number, b: number) => number, opName: string = ''): Series {
if (typeof other === 'number')
return this.map((val: number) => op(val, other));
else if (other instanceof Series)
return this.map((val: number, idx: number) => op(val, other.iloc(idx)));
else if (Array.isArray(other))
return this.map((val: number, idx: number) => op(val, other[idx]));
else if (other instanceof Immutable.List)
return this.map((val: number, idx: number) => op(val, other.get(idx)));
throw new Error(`${opName} only supports numbers, Arrays, Immutable List and pandas.Series`);
}
/**
* Add another Iterable, `Series`, or number to the `Series`
*
* pandas equivalent: [Series.add](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.add.html)
*
* @param {Iterable|Series|number} other
* Value to add to the `Series`
*
* @returns {Series}
*
* @example
* const ds = new Series([1, 2, 3], {name: 'New Series'})
* ds.add(5) // Series([6, 7, 8], {name: 'New Series'})
* ds.add(new Series([2, 3, 4])) // Series([3, 5, 7], {name: 'New Series'})
* ds.add([2, 3, 4]) // Series([3, 5, 7], {name: 'New Series'})
* ds.add(Immutable.List([2, 3, 4])) // Series([3, 5, 7], {name: 'New Series'})
*/
add(other: any): Series {
return this._combineOp(other, (a: number, b: number) => a + b, 'add');
}
/**
* Subtract another Iterable, `Series`, or number from the `Series`
*
* pandas equivalent: [Series.sub](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.sub.html)
*
* @param {Iterable|Series|number} other
* Value to subtract from the `Series`
*
* @returns {Series}
*
* @example
* const ds = new Series([1, 2, 3], {name: 'New Series'})
*
* ds.sub(5) // Series([-4, -3, -2], {name: 'New Series'})
* ds.sub(new Series([2, 3, 4])) // Series([-1, -1, -1], {name: 'New Series'})
* ds.sub([2, 3, 4]) // Series([-1, -1, -1], {name: 'New Series'})
* ds.sub(Immutable.List([2, 3, 4])) // Series([-1, -1, -1], {name: 'New Series'})
*/
sub(other: any): Series {
return this._combineOp(other, (a: number, b: number) => a - b, 'sub');
}
/**
* Multiply by another Iterable, `Series`, or number from the `Series`
*
* pandas equivalent: [Series.mul](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.mul.html)
*
* @param {Iterable|Series|number} other
* Value to multiply by the `Series`
*
* @returns {Series}
*
* @example
* const ds = new Series([1, 2, 3], {name: 'New Series'})
*
* ds.mul(5) // Series([5, 10, 15], {name: 'New Series'})
* ds.mul(new Series([2, 3, 4])) // Series([2, 6, 12], {name: 'New Series'})
* ds.mul([2, 3, 4]) // Series([2, 6, 12], {name: 'New Series'})
* ds.mul(Immutable.List([2, 3, 4])) // Series([2, 6, 12], {name: 'New Series'})
*/
mul(other: any): Series {
return this._combineOp(other, (a: number, b: number) => a * b);
}
/**
* Multiply by another Iterable, `Series`, or number from the `Series`
*
* pandas equivalent: [Series.multiply](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.multiply.html)
*
* @param {Iterable|Series|number} other
* Value to multiply by the `Series`
*
* @returns {Series}
*
* @example
* const ds = new Series([1, 2, 3], {name: 'New Series'})
*
* ds.multiply(5) // Series([5, 10, 15], {name: 'New Series'})
* ds.multiply(new Series([2, 3, 4])) // Series([2, 6, 12], {name: 'New Series'})
* ds.multiply([2, 3, 4]) // Series([2, 6, 12], {name: 'New Series'})
* ds.multiply(Immutable.List([2, 3, 4])) // Series([2, 6, 12], {name: 'New Series'})
*/
multiply(other: any): Series {
return this.mul(other);
}
/**
* Divide by another Iterable, `Series`, or number from the `Series`
*
* pandas equivalent: [Series.div](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.div.html)
*
* @param {Iterable|Series|number} other
* Value by which to divide the `Series`
*
* @returns {Series}
*
* @example
* const ds = new Series([1, 2, 3], {name: 'New Series'})
*
* ds.div(5) // Series([0.2, 0.4, 0.6], {name: 'New Series'})
* ds.div(new Series([4, 2, 1])) // Series([0.25, 1, 3], {name: 'New Series'})
* ds.div([4, 2, 1]) // Series([0.25, 1, 3], {name: 'New Series'})
* ds.div(Immutable.List([4, 2, 1])) // Series([0.25, 1, 3], {name: 'New Series'})
*/
div(other: any): Series {
return this._combineOp(other, (a: number, b: number) => a / b, 'div');
}
/**
* Divide by another Iterable, `Series`, or number from the `Series`
*
* pandas equivalent: [Series.divide](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.divide.html)
*
* @param {Iterable|Series|number} other
* Value by which to divide the `Series`
*
* @returns {Series}
*
* @example
* const ds = new Series([1, 2, 3], {name: 'New Series'})
*
* ds.divide(5) // Series([0.2, 0.4, 0.6], {name: 'New Series'})
* ds.divide(new Series([4, 2, 1])) // Series([0.25, 1, 3], {name: 'New Series'})
* ds.divide([4, 2, 1]) // Series([0.25, 1, 3], {name: 'New Series'})
* ds.divide(Immutable.List([4, 2, 1])) // Series([0.25, 1, 3], {name: 'New Series'})
*/
divide(other: any): Series {
return this.div(other);
}
/**
* Calculate the covariance between this `Series` and another `Series` or iterable
*
* pandas equivalent: [Series.cov](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.cov.html)
*
* @param {Series} ds
* Series with which to calculate covariance
*
* @returns {number}
*
* @example
* const ds1 = new Series([1, 2, 3, 4, 5]);
* const ds2 = new Series([2, 4, 6, 8, 10]);
*
* // Returns 5
* ds1.cov(ds2);
*
* // Also returns 5
* ds2.cov(ds1);
*/
cov(ds: Series): number {
if (!(ds instanceof Series))
throw new Error('ds must be a Series');
if (ds.length !== this.length)
throw new Error('Series must be of equal length');
let n = 0;
let mean1 = 0;
let mean2 = 0;
let m12 = 0;
this.values.forEach((v1, idx) => {
n += 1;
const d1 = (v1 - mean1) / n;
mean1 += d1;
const d2 = (ds.values.get(idx) - mean2) / n;
mean2 += d2;
m12 += ((((n - 1) * d1) * d2) - (m12 / n));
});
return (n / (n - 1)) * m12;
}
/**
* Calculate the correlation between this `Series` and another `Series` or iterable
*
* pandas equivalent: [Series.corr](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.corr.html)
*
* @param {Series} ds
* Series with which to calculate correlation
*
* @returns {number}
*
* @example
* const ds1 = new Series([1, 2, 3, 4, 5]);
* const ds2 = new Series([2, 4, 6, 8, 10]);
*
* // Returns 1
* ds1.corr(ds2);
*
* // Also returns 1
* ds2.corr(ds1);
*/
corr(ds: Series): number {
if (!(ds instanceof Series))
throw new Error('ds must be a Series');
if (ds.length !== this.length)
throw new Error('Series must be of equal length');
return this.cov(ds) / (this.std() * ds.std());
}
/**
* Return the difference over a given number of periods
*
* pandas equivalent: [Series.diff](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.diff.html)
*
* @param {number} periods=1
* Number of periods to use for difference calculation
*
* @returns {Series}
*
* @example
* const ds = new Series([1, 2, 6, 5])
*
* // Returns Series([null, 1, 4, -1])
* ds.diff();
*
* // Returns Series([null, null, 5, 3])
* ds.diff(2);
*/
diff(periods: number = 1): Series {
if (typeof periods !== 'number' || !Number.isInteger(periods))
throw new Error('periods must be an integer');
if (periods <= 0)
throw new Error('periods must be positive');
return new Series(
Immutable.Repeat(null, periods).toList().concat(
Immutable.Range(periods, this.length).map(idx =>
(this.values.get(idx) - this.values.get(idx - periods))).toList()),
{index: this.index, name: this.name});
}
/**
* Return the percentage change over a given number of periods
*
* pandas equivalent: [Series.pct_change](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.pct_change.html)
*
* @param {number} periods=1
* Number of periods to use for percentage change calculation
*
* @returns {Series}
*
* @example
* const ds = new Series([1, 2, 3, 4, 5], {name: 'New Series'})
*
* ds.pct_change(1) // Series([null, 1, 0.5, 0.333, 0.25], {name: 'New Series'})
* ds.pct_change(2) // Series([null, null, 2, 1, 0.66666], {name: 'New Series'})
*/
pct_change(periods: number = 1): Series {
if (typeof periods !== 'number' || !Number.isInteger(periods))
throw new Error('periods must be an integer');
if (periods <= 0)
throw new Error('periods must be positive');
return new Series(
Immutable.Repeat(null, periods).toList().concat(
Immutable.Range(periods, this.length).map(idx =>
(this.values.get(idx) / this.values.get(idx - periods)) - 1).toList()),
{index: this.index, name: this.name});
}
_sort_ascending(valueA: number, valueB: number) {
const valA = this.iloc(valueA);
const valB = this.iloc(valueB);
// $FlowFixMe
if (valA < valB) return -1;
// $FlowFixMe
else if (valA > valB) return 1;
return 0;
}
_sort_descending(valueA: number, valueB: number) {
const valA = this.iloc(valueA);
const valB = this.iloc(valueB);
// $FlowFixMe
if (valA > valB) return -1;
// $FlowFixMe
else if (valA < valB) return 1;
return 0;
}
/**
* Return a sorted `Series` in either ascending or descending order
*
* pandas equivalent: [Series.sort_values](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.sort_values.html)
*
* @param {boolean} ascending
* Sort in ascending (true) or descending (false) order
*
* @returns {Series}
*
* @example
* const ds = new Series([2, 1, 0, 3], {name: 'New Series', index: [0, 1, 2, 3]})
*
* ds.sort_values(true) // Series([0, 1, 2, 3], {name: 'New Series', index: [2, 1, 0, 3]})
* ds.sort_values(false) // Series([3, 2, 1, 0], {name: 'New Series', index: [3, 0, 1, 2]})
*/
sort_values(ascending: boolean = true): Series {
const sortedIndex = ascending
? this.index.sort(this._sort_ascending)
: this.index.sort(this._sort_descending);
return new Series(sortedIndex.map(i => this.iloc(i)), {name: this.name, index: sortedIndex});
}
/**
* Return a `Series` with all values rounded to the nearest precision specified by decimals
*
* pandas equivalent: [Series.round](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.round.html)
*
* @param {number} decimals=0
* Number of decimals to round to
*
* @example
* const ds = new Series([1.25, 1.47, 1.321])
*
* // Returns Series([1.3, 1.5, 1.3])
* ds.round(1);
*/
round(decimals: number = 0): Series {
return new Series(this.values.map(v => round10(v, -1 * decimals)));
}
// Filtering methods
_alignSeries(series: Series): Immutable.Map {
// Align two series by index values, returning a Map with index values as keys and
// values as Maps with 1: List [value locations at index], 2: [value locations at index]
let seriesAlignment = Immutable.Map({});
this.index.forEach((idx1) => {
if (!(seriesAlignment.has(idx1))) {
seriesAlignment = seriesAlignment
.set(idx1, Immutable.Map({
first: Immutable.List.of(this.iloc(idx1)),
second: Immutable.List([]),
}));
} else {
seriesAlignment = seriesAlignment.updateIn([idx1, 'first'], l => l.concat(this.iloc(idx1)));
}
});
series.index.forEach((idx2) => {
if (!(seriesAlignment.has(idx2))) {
seriesAlignment = seriesAlignment
.set(idx2, Immutable.Map({
first: Immutable.List([]),
second: Immutable.List.of(series.iloc(idx2)),
}));
} else {
seriesAlignment = seriesAlignment.updateIn([idx2, 'second'],
l => l.concat(series.iloc(idx2)));
}
});
return seriesAlignment;
}
/**
* Flexible comparison of an iterable or value to the `Series`. Returns a `Series` of booleans of
* equivalent length
*
* pandas equivalent: [Series.where](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.where.html)
*
* @param {Series|Array|List|string|number} other
* Iterable or value compared to Series
* @param {function} op
* Function which takes (a, b) values and returns a boolean
* @returns {Series}
*
* @example
* const ds = new Series([1, 2, 3], {name: 'Test name'})
*
* // Returns Series([true, false, false])
* ds.where(1, (v1, v2) => v1 === 1);
*
* // Returns Series([false, true, true])
* ds.where(new Series([0, 2, 3]), (v1, v2) => v1 === v2);
*/
where(other: any, op: T_BF) {
const name = this.name;
const index = this.index;
const kwargs = {name, index};
if (!(Array.isArray(other)) && !(other instanceof Immutable.List) && !(other instanceof Series))
return new Series(this.values.map(v => op(v, other)), kwargs);
if (Array.isArray(other)) {
if (other.length !== this.length) throw new Error('Must be equal length for comparison');
return new Series(this.values.map((v, idx) => op(v, other[idx])), kwargs);
} else if (other instanceof Immutable.List) {
if (other.size !== this.length) throw new Error('Must be equal length for comparison');
return new Series(this.values.map((v, idx) => op(v, other.get(idx))), kwargs);
} else if (other instanceof Series) {
if (other.length !== this.length) throw new Error('Must be equal length for comparison');
return new Series(this.values.map((v, idx) => op(v, other.iloc(idx))), kwargs);
}
throw new Error('Must be scalar value, Array, Series, or Immutable.List');
}
/**
* Equal to of `Series` and other, element wise
*
* pandas equivalent: Series == val
*
* @param {Series|Array|List|number|string} other
* Other `Series` or scalar value to check for equality
*
* @returns {Series}
*
* @example
* const ds = new Series([1, 2, 3], {name: 'Test name'})
*
* // Returns Series([true, false, false])
* ds.eq(1);
*
* // Returns Series([false, true, true])
* ds.eq(new Series([0, 2, 3]));
*
* // Returns Series([false, true, true])
* ds.eq(Immutable.List([0, 2, 3]));
*
* // Returns Series([false, true, true])
* ds.eq([0, 2, 3]);
*/
eq(other: any): Series {
return this.where(other, (a, b) => a === b);
}
/**
* Less than of `Series` and other, element wise
*
* pandas equivalent: Series < val
*
* @param {Series|Array|List|number|string} other
* Other `Series` or scalar value to check for less than
*
* @returns {Series}
*
* @example
* const ds = new Series([1, 2, 3], {name: 'Test name'})
*
* // Returns Series([false, false, false])
* ds.lt(1);
*
* // Returns Series([false, false, true])
* ds.lt(new Series([0, 2, 4]));
*
* // Returns Series([false, false, true])
* ds.lt(Immutable.List([0, 2, 5]));
*
* // Returns Series([false, false, true])
* ds.lt([0, 2, 5]);
*/
lt(other: any): Series {
return this.where(other, (a, b) => a < b);
}
/**
* Less than or equal to of `Series` and other, element wise
*
* pandas equivalent: Series <= val
*
* @param {Series|Array|List|number|string} other
* Other `Series` or scalar value to check for less than or equal to
*
* @returns {Series}
*
* @example
* const ds = new Series([1, 2, 3], {name: 'Test name'})
*
* // Returns Series([false, false, false])
* ds.lte(1);
*
* // Returns Series([false, false, true])
* ds.lte(new Series([0, 2, 4]));
*
* // Returns Series([false, false, true])
* ds.lte(Immutable.List([0, 2, 5]));
*
* // Returns Series([false, false, true])
* ds.lte([0, 2, 5]);
*/
lte(other: any): Series {
return this.where(other, (a, b) => a <= b);
}
/**
* Greater than of `Series` and other, element wise
*
* pandas equivalent: Series > val
*
* @param {Series|Array|List|number|string} other
* Other `Series` or scalar value to check for greater than
*
* @returns {Series}
*
* @example
* const ds = new Series([1, 2, 3], {name: 'Test name'})
*
* // Returns Series([false, true, true])
* ds.gt(1);
*
* // Returns Series([true, false, false])
* ds.gt(new Series([0, 2, 3]));
*
* // Returns Series([true, false, false])
* ds.gt(Immutable.List([0, 2, 3]));
*
* // Returns Series([true, false, false])
* ds.gt([0, 2, 3]);
*/
gt(other: any): Series {
return this.where(other, (a, b) => a > b);
}
/**
* Greater than or equal to of `Series` and other, element wise
*
* pandas equivalent: Series >= val
*
* @param {Series|Array|List|number|string} other
* Other `Series` or scalar value to check for greater than or equal to
*
* @returns {Series}
*
* @example
* const ds = new Series([1, 2, 3], {name: 'Test name'})
*
* // Returns Series([true, true, true])
* ds.gte(1);
*
* // Returns Series([true, true, false])
* ds.gte(new Series([0, 2, 4]));
*
* // Returns Series([true, true, false])
* ds.gte(Immutable.List([0, 2, 4]));
*
* // Returns Series([true, true, false])
* ds.gte([0, 2, 4]);
*/
gte(other: any): Series {
return this.where(other, (a, b) => a >= b);
}
/**
* Returns a boolean same-sized Series indicating if the values are not null
*
* pandas equivalent: [Series.notnull](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.notnull.html)
*
* @returns {Series}
*
* @example
* const ds = new Series([1, 2, null, null, 4]);
*
* // Returns Series([true, true, false, false, true])
* ds.notnull();
*/
notnull(): Series {
return this.where(null, (a, b) => a !== b);
}
/**
* Shift index by desired number of periods
*
* pandas equivalent:s [Series.shift](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.shift.html)
*
* @param {number} periods
* Number of periods to move, can be positive or negative
*
* @returns {Series}
*
* @example
* const ds = new Series([1, 2, 3, 4]);
*
* // Returns Series([null, 1, 2, 3]);
* ds.shift(1);
*
* // Returns Series([null, null, 1, 2]);
* ds.shift(2);
*
* // Returns Series([3, 4, null, null]);
* ds.shift(-2);
*/
shift(periods: number = 1): Series {
if (!Number.isInteger(periods)) throw new Error('periods must be an integer');
if (periods === 0) {
return this.copy();
} else if (periods < 0) {
const absPeriods = Math.abs(periods);
if (absPeriods > this.length) throw new Error('Periods greater than length of Series');
const values = this.values.slice(absPeriods, this.length)
.concat(Immutable.Repeat(null, absPeriods).toList());
return new Series(values, {name: this.name, index: this.index});
}
// periods > 0
if (periods > this.length) throw new Error('Periods greater than length of Series');
const values = Immutable.Repeat(null, periods).toList()
.concat(this.values.slice(0, this.length - periods));
return new Series(values, {name: this.name, index: this.index});
}
/**
* Returns `Immutable.List` of unique values in the `Series`. Preserves order of the original
*
* pandas equivalent: [Series.unique](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.unique.html)
*
* @returns {List}
*
* @example
* const ds = new Series(['foo', 'bar', 'bar', 'foo', 'foo', 'test', 'bar', 'hi']);
* // Returns ['foo', 'bar', 'test', 'hi']
* ds.unique();
*/
unique(): Immutable.List {
return this.values.toSet().toList();
}
/**
* Filter the Series by an Iterable (Series, Array, or List) of booleans and return the subset
*
* pandas equivalent: series[series condition]
*
* @param {Series|Array|List} iterBool
* Iterable of booleans
*
* @returns {Series}
*
* @example
* const ds = new Series([1, 2, 3]);
*
* // Returns Series([2, 3]);
* ds.filter(ds.gte(2));
*/
filter(iterBool: Series | Array<boolean> | Immutable.List): Series {
if (!Array.isArray(iterBool)
&& !(iterBool instanceof Immutable.List)
&& !(iterBool instanceof Series))
throw new Error('filter must be an Array, List, or Series');
const valueIndexMap = {values: [], index: []};
if (iterBool instanceof Series)
iterBool.values.forEach((v, idx) => {
if (v === true) {
valueIndexMap.values.push(this.values.get(idx));
valueIndexMap.index.push(this.index.get(idx));
}
});
else {
iterBool.forEach((v, idx) => {
if (v === true) {
valueIndexMap.values.push(this.values.get(idx));
valueIndexMap.index.push(this.index.get(idx));
}
});
}
return new Series(valueIndexMap.values, {name: this.name, index: valueIndexMap.index});
}
_cumulativeHelper(operation: string = OP_CUMSUM): Series {
return new Series(generateCumulativeFunc(operation)(this.values), this.kwargs);
}
/**
* Return cumulative sum over requested axis
*
* pandas equivalent: [Series.cumsum](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.cumsum.html)
*
* @returns {Series}
*
* @example
* const ds = new Series([1, 2, 3], {index: [2, 3, 4]});
*
* // Returns Series([1, 3, 6], {index: [2, 3, 4]});
* ds.cumsum();
*/
cumsum(): Series {
return this._cumulativeHelper(OP_CUMSUM);
}
/**
* Return cumulative mul over requested axis
*
* pandas equivalent: [Series.cummul](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.cummul.html)
*
* @returns {Series}
*
* @example
* const ds = new Series([1, 2, 3], {index: [2, 3, 4]});
*
* // Returns Series([1, 2, 6], {index: [2, 3, 4]});
* ds.cummul();
*/
cummul(): Series {
return this._cumulativeHelper(OP_CUMMUL);
}
/**
* Return cumulative max over requested axis
*
* pandas equivalent: [Series.cummax](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.cummax.html)
*
* @returns {Series}
*
* @example
* const ds = new Series([3, 2, 4], {index: [2, 3, 4]});
*
* // Returns Series([3, 3, 4], {index: [2, 3, 4]});
* ds.cummax();
*/
cummax(): Series {
return this._cumulativeHelper(OP_CUMMAX);
}
/**
* Return cumulative min over requested axis
*
* pandas equivalent: [Series.cummin](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.cummin.html)
*
* @returns {Series}
*
* @example
* const ds = new Series([1, 2, 3], {index: [2, 3, 4]});
*
* // Returns Series([1, 1, 1], {index: [2, 3, 4]});
* ds.cummin();
*/
cummin(): Series {
return this._cumulativeHelper(OP_CUMMIN);
}
/**
* Convert the Series to a json object
*
* pandas equivalent: [Series.to_json](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.to_json.html)
*
* @param kwargs
* @param {string} [kwargs.orient=columns] orientation of JSON
*
* @returns {*}
*
* @example
* const ds = new Series([1, 2, 3], {name: 'x'});
*
* // Returns {0: 1, 1: 2, 2: 3}
* ds.to_json();
*
* // Returns [1, 2, 3]
* ds.to_json({orient: 'records'});
*
* // Returns {index: [0, 1, 2], name: 'x', values: [1, 2, 3]}
* ds.to_json({orient: 'split'});
*/
to_json(kwargs: Object = {orient: 'index'}): Object {
const ALLOWED_ORIENT = ['records', 'split', 'index'];
let orient = 'index';
if (typeof kwargs.orient !== 'undefined') {
if (ALLOWED_ORIENT.indexOf(kwargs.orient) < 0) // $FlowFixMe TODO
throw new TypeError(`orient must be in ${ALLOWED_ORIENT}`);
orient = kwargs.orient;
}
let json;
switch (orient) {
case 'records':
return this.values.toArray();
case 'split':
return {index: this.index.toArray(), name: this.name, values: this.values.toJS()};
case 'index':
json = {};
this.values.forEach((v, idx) => {
json[this.index.get(idx)] = v;
});
return json;
default:
// $FlowFixMe TODO
throw new TypeError(`orient must be in ${ALLOWED_ORIENT}`);
}
}
/**
* Rename the `Series` and return a new `Series`
*
* pandas equivalent: [Series.rename](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.rename.html)
*
* @param {string} name
*
* @example
* const ds = new Series([1, 2, 3], {name: 'Test name'});
* ds.rename('New test name');
* // returns 'New test name'
* ds.name;
*/
rename(name: string | number): Series {
return new Series(this._values, {name, index: this.index});
}
/**
* Append another Series to this and return a new Series
*
* pandas equivalent: [Series.append](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.append.html)
*
* @param {Series} other
* @param {boolean} ignore_index
* @returns {Series}
*
* @example
* const ds1 = new Series([1, 2, 3], {index: [1, 2, 3]});
* const ds2 = new Series([2, 3, 4], {index: [3, 4, 5]});
*
* // Returns Series([1, 2, 3, 2, 3, 4], {index: [1, 2, 3, 3, 4, 5]});
* ds1.append(ds2);
*
* // Returns Series([1, 2, 3, 2, 3, 4], {index: [0, 1, 2, 3, 4, 5]});
* ds1.append(ds2, true);
*/
append(other: Series, ignore_index: boolean = false): Series {
// eslint-disable-next-line
return _concatSeries( // $FlowFixMe
[this, other],
{ignore_index});
}
}
type T_ITER_SERIES = Array<Series> | Immutable.List<Series>;
type T_KWARGS = {ignore_index: boolean, axis?: 0 | 1};
const _concatSeriesValues = (objs: T_ITER_SERIES) =>
Immutable.List([]).concat(...objs.map(series => series.values));
const _concatSeriesIndices = (objs: T_ITER_SERIES) =>
Immutable.List([]).concat(...objs.map(series => series.index));
export const _concatSeries = (objs: Array<Series> | Immutable.List<Series>,
kwargs: T_KWARGS): Series => {
if (objs instanceof Immutable.List
&& objs.filter(series => series instanceof Series).size !== objs.size)
throw new Error('Objects must all be Series');
else if (Array.isArray(objs)
&& objs.filter(series => series instanceof Series).length !== objs.length)
throw new Error('Objects must all be Series');
if (!kwargs.ignore_index)
return new Series(_concatSeriesValues(objs), {index: _concatSeriesIndices(objs)});
else if (kwargs.ignore_index) {
return new Series(
_concatSeriesValues(objs),
{index: Immutable.Range(0, objs.reduce((a, b: Series) => a + b.length, 0)).toList()});
}
throw new Error('Not supported');
};