javascriptdata / danfojs

Danfo.js is an open source, JavaScript library providing high performance, intuitive, and easy to use data structures for manipulating and processing structured data.
https://danfo.jsdata.org/
MIT License
4.79k stars 209 forks source link

Type Narrowing an object which is either dfd.Series or dfd.DataFrame to dfd.DataFrame #640

Open bml1g12 opened 6 months ago

bml1g12 commented 6 months ago

Is your feature request related to a problem? Please describe.

I am unclear on the recommended way of type narrowing a dfd.Series ❘ dfd.DataFrame object to a dfd.DataFrame

Describe the solution you'd like

e.g. this should work, or some other approach that achieves the type narrowing

  if (!(multiMetricDf instanceof dfd.DataFrame)) {
    throw new Error(
      `Expected a DataFrame here but got ${multiMetricDf.constructor.name}`,
    )
  }

The above code snippet does not work as it can log " "Expected a DataFrame here but got DataFrame"" i.e. false positive, and I would like a solution like this to type narrow

Describe alternatives you've considered

const dfd = require("danfojs-node");

const json_data = [
  { A: 0.4612, B: 4.28283, C: -1.509, D: -1.1352 },
  { A: 0.5112, B: -0.22863, C: -3.39059, D: 1.1632 },
  { A: 0.6911, B: -0.82863, C: -1.5059, D: 2.1352 },
  { A: 0.4692, B: -1.28863, C: 4.5059, D: 4.1632 },
];

const df = new dfd.DataFrame(json_data)
const multiMetricDf = dfd.concat({
  dfList: [df, df],
  axis: 0,
})
multiMetricDf instanceof dfd.DataFrame // returns False

The above code snippet returns False

If there was a method to convert Series to DataFrame (https://github.com/javascriptdata/danfojs/issues/619) that would be a suitable workaround

Additional context

For working with Danfo, I think such type narrowing is essential

const dfd = require("danfojs-node");

function logPrototypeNames(obj: any): void {
  let proto = Object.getPrototypeOf(obj);

  while (proto) {
      console.log(`Prototype constructor name: ${proto.constructor.name}`);
      proto = Object.getPrototypeOf(proto);
  }
}

const json_data = [
  { A: 0.4612, B: 4.28283, C: -1.509, D: -1.1352 },
  { A: 0.5112, B: -0.22863, C: -3.39059, D: 1.1632 },
  { A: 0.6911, B: -0.82863, C: -1.5059, D: 2.1352 },
  { A: 0.4692, B: -1.28863, C: 4.5059, D: 4.1632 },
];
const df = new dfd.DataFrame(json_data)

console.log("df is instance of DataFrame:", df instanceof dfd.DataFrame);

logPrototypeNames(df);
console.log()

const multiMetricDf = dfd.concat({
  dfList: [df, df],
  axis: 0,
})
multiMetricDf instanceof dfd.DataFrame

console.log("multiMetricDf is NOT instance of DataFrame:", multiMetricDf instanceof dfd.DataFrame);

logPrototypeNames(multiMetricDf);

console.log("here", multiMetricDf.ctypes)

Returns

df is instance of DataFrame: true
Prototype constructor name: DataFrame
Prototype constructor name: DataFrame
Prototype constructor name: NDframe
Prototype constructor name: Object

multiMetricDf is NOT instance of DataFrame: false
Prototype constructor name: DataFrame
Prototype constructor name: NDframe
Prototype constructor name: Object

So not sure what the problem is here.

This is my current workaround:

/**
 * Allows type narrowing of Series to DataFrame
 * https://github.com/javascriptdata/danfojs/issues/640
 *
 * @param df - DataFrame or Series
 * @returns True if it has a ctypes attribute, expected on DataFrame but not Series
 */
function isDataFrame(df: unknown): df is dfd.DataFrame {
  if (df instanceof dfd.Series) {
    return false
  }
  if ((df as dfd.DataFrame).ctypes !== undefined) {
    // ctypes attribute only exists on DataFrame
    return true
  } else {
    return false
  }
}
bml1g12 commented 6 months ago

Maybe the issue is related to a difference between

import { DataFrame } from "danfojs-node/dist/danfojs-base"
import { DataFrame } from "danfojs-node"

which means that the latter is not equal to the former, and the former is returned in some cases, whereas the latter is imported by default